From 4443394e6f07f54e28d3257c02a1121452f33a32 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Fri, 29 Mar 2019 12:37:28 +0300 Subject: [PATCH] feat(typo): add merging typos --- back/src/mind/application/TypoMerger.ts | 67 +++++++++++++++++++ back/src/mind/mind.module.ts | 5 +- .../http/controller/TypoController.ts | 32 +++++++++ .../http/request/MergeTypoRequest.ts | 11 +++ back/src/money/domain/Income.entity.ts | 2 +- back/src/money/domain/IncomeRepository.ts | 13 ++++ back/src/money/domain/Outcome.entity.ts | 2 +- back/src/money/domain/OutcomeRepository.ts | 13 ++++ front/src/domain/mind/actions/mergeTypos.ts | 12 ++-- front/src/domain/mind/api/mergeTypoRequest.ts | 7 ++ .../now/tips/components/merge/Merge.tsx | 7 +- shared/models/mind/MergeTypoModel.ts | 4 ++ 12 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 back/src/mind/application/TypoMerger.ts create mode 100644 back/src/mind/presentation/http/controller/TypoController.ts create mode 100644 back/src/mind/presentation/http/request/MergeTypoRequest.ts create mode 100644 front/src/domain/mind/api/mergeTypoRequest.ts create mode 100644 shared/models/mind/MergeTypoModel.ts diff --git a/back/src/mind/application/TypoMerger.ts b/back/src/mind/application/TypoMerger.ts new file mode 100644 index 00000000..8c555cda --- /dev/null +++ b/back/src/mind/application/TypoMerger.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common' + +import { IncomeRepository } from '@back/money/domain/IncomeRepository' +import { OutcomeRepository } from '@back/money/domain/OutcomeRepository' +import { EntitySaver } from '@back/db/EntitySaver' + +@Injectable() +export class TypoMerger { + public constructor( + private readonly incomeRepo: IncomeRepository, + private readonly outcomeRepo: OutcomeRepository, + private readonly entitySaver: EntitySaver, + ) {} + + public async merge( + primary: string, + secondary: string[], + userLogin: string, + ): Promise { + await Promise.all([ + this.mergeInIncomes(primary, secondary, userLogin), + this.mergeInOutcomes(primary, secondary, userLogin), + ]) + } + + private async mergeInIncomes( + primary: string, + secondary: string[], + userLogin: string, + ): Promise { + const [mainIncomes, incomes] = await Promise.all([ + this.incomeRepo.findBySourcesForUser([primary], userLogin), + this.incomeRepo.findBySourcesForUser(secondary, userLogin), + ]) + + if (mainIncomes.length === 0) { + return + } + + incomes.forEach(income => { + income.source = primary + }) + + await this.entitySaver.save(...incomes) + } + + private async mergeInOutcomes( + primary: string, + secondary: string[], + userLogin: string, + ): Promise { + const [mainOutcomes, outcomes] = await Promise.all([ + this.outcomeRepo.findByCategoriesForUser([primary], userLogin), + this.outcomeRepo.findByCategoriesForUser(secondary, userLogin), + ]) + + if (mainOutcomes.length === 0) { + return + } + + outcomes.forEach(outcome => { + outcome.category = primary + }) + + await this.entitySaver.save(...outcomes) + } +} diff --git a/back/src/mind/mind.module.ts b/back/src/mind/mind.module.ts index ed25e30f..b5a15874 100644 --- a/back/src/mind/mind.module.ts +++ b/back/src/mind/mind.module.ts @@ -14,6 +14,8 @@ import { TipsFilter } from './application/TipsFilter' import { DisabledTip } from './domain/DisabledTip.entity' import { DisabledTipRepository } from './domain/DisabledTipRepository' import { TipsDisabler } from './application/TipsDisabler' +import { TypoController } from './presentation/http/controller/TypoController' +import { TypoMerger } from './application/TypoMerger' @Module({ imports: [ @@ -22,9 +24,10 @@ import { TipsDisabler } from './application/TipsDisabler' MoneyModule, TypeOrmModule.forFeature([DisabledTip]), ], - controllers: [TipController], + controllers: [TipController, TypoController], providers: [ TypoFinder, + TypoMerger, TypoAdviser, AdviserUnity, TipsFilter, diff --git a/back/src/mind/presentation/http/controller/TypoController.ts b/back/src/mind/presentation/http/controller/TypoController.ts new file mode 100644 index 00000000..f5e3ae75 --- /dev/null +++ b/back/src/mind/presentation/http/controller/TypoController.ts @@ -0,0 +1,32 @@ +import { Controller, Get, Body, Post } from '@nestjs/common' +import { + ApiUseTags, + ApiBearerAuth, + ApiOperation, + ApiOkResponse, +} from '@nestjs/swagger' + +import { OnlyForUsers } from '@back/user/presentation/http/security/OnlyForUsers' +import { TokenPayload } from '@back/user/application/dto/TokenPayload' +import { CurrentUser } from '@back/user/presentation/http/decorator/CurrentUser' + +import { MergeTypoRequest } from '../request/MergeTypoRequest' +import { TypoMerger } from '@back/mind/application/TypoMerger' + +@Controller('mind/typo') +@OnlyForUsers() +@ApiUseTags('mind') +@ApiBearerAuth() +export class TypoController { + public constructor(private readonly merger: TypoMerger) {} + + @Post('merge') + @ApiOperation({ title: 'Merge typos' }) + @ApiOkResponse({ description: 'Merges' }) + public async showAll( + @CurrentUser() user: TokenPayload, + @Body() request: MergeTypoRequest, + ): Promise { + await this.merger.merge(request.primary, request.secondary, user.login) + } +} diff --git a/back/src/mind/presentation/http/request/MergeTypoRequest.ts b/back/src/mind/presentation/http/request/MergeTypoRequest.ts new file mode 100644 index 00000000..ad051f57 --- /dev/null +++ b/back/src/mind/presentation/http/request/MergeTypoRequest.ts @@ -0,0 +1,11 @@ +import { ApiModelProperty } from '@nestjs/swagger' + +import { MergeTypoModel } from '@shared/models/mind/MergeTypoModel' + +export class MergeTypoRequest implements MergeTypoModel { + @ApiModelProperty({ example: 'Lunch' }) + public readonly primary: string + + @ApiModelProperty({ example: ['lunch', 'Lonch'] }) + public readonly secondary: string[] +} diff --git a/back/src/money/domain/Income.entity.ts b/back/src/money/domain/Income.entity.ts index 51be7408..310ca02b 100644 --- a/back/src/money/domain/Income.entity.ts +++ b/back/src/money/domain/Income.entity.ts @@ -20,7 +20,7 @@ export class Income implements AbstractTransaction { public readonly date: Date @Column() - public readonly source: string + public source: string @ManyToOne(type => User) public readonly author: User diff --git a/back/src/money/domain/IncomeRepository.ts b/back/src/money/domain/IncomeRepository.ts index 87376b60..db4cf162 100644 --- a/back/src/money/domain/IncomeRepository.ts +++ b/back/src/money/domain/IncomeRepository.ts @@ -73,6 +73,19 @@ class IncomeRepo implements TransactionRepository { return result.map(({ source }) => source) } + + public async findBySourcesForUser( + sources: string[], + userLogin: string, + ): Promise { + return this.incomeRepo + .createQueryBuilder('income') + .where('income.source IN (:...sources)', { sources }) + .innerJoin('income.author', 'author', 'author.login = :userLogin', { + userLogin, + }) + .getMany() + } } export const IncomeRepository = IncomeRepo diff --git a/back/src/money/domain/Outcome.entity.ts b/back/src/money/domain/Outcome.entity.ts index 7d3236c0..8591217b 100644 --- a/back/src/money/domain/Outcome.entity.ts +++ b/back/src/money/domain/Outcome.entity.ts @@ -20,7 +20,7 @@ export class Outcome implements AbstractTransaction { public readonly date: Date @Column() - public readonly category: string + public category: string @ManyToOne(type => User) public readonly author: User diff --git a/back/src/money/domain/OutcomeRepository.ts b/back/src/money/domain/OutcomeRepository.ts index 50b8705f..b901bc25 100644 --- a/back/src/money/domain/OutcomeRepository.ts +++ b/back/src/money/domain/OutcomeRepository.ts @@ -73,6 +73,19 @@ class OutomeRepo implements TransactionRepository { return result.map(({ category }) => category) } + + public async findByCategoriesForUser( + categories: string[], + userLogin: string, + ): Promise { + return this.outcomeRepo + .createQueryBuilder('outcome') + .where('outcome.category IN (:...categories)', { categories }) + .innerJoin('outcome.author', 'author', 'author.login = :userLogin', { + userLogin, + }) + .getMany() + } } export const OutcomeRepository = OutomeRepo diff --git a/front/src/domain/mind/actions/mergeTypos.ts b/front/src/domain/mind/actions/mergeTypos.ts index dbe75dce..8aeeb9f7 100644 --- a/front/src/domain/mind/actions/mergeTypos.ts +++ b/front/src/domain/mind/actions/mergeTypos.ts @@ -1,11 +1,15 @@ +import { MergeTypoModel } from '@shared/models/mind/MergeTypoModel' +import { refetchData } from '@front/domain/money/actions/refetchData' import { fetchOrFail } from '@front/domain/store' import { actions } from '../reducer/tips' +import { mergeTypoRequest } from '../api/mergeTypoRequest' -export const mergeTypos = (token: string, main: string, other: string[]) => - fetchOrFail(actions.fetching, async dispatch => { - // TODO: real merging please =) - console.log(main, other) +export const mergeTypos = (token: string, merge: MergeTypoModel) => + fetchOrFail(actions.fetching, async (dispatch, getApi) => { + await mergeTypoRequest(getApi())(merge) dispatch(actions.data.removeTips([token])) + + await dispatch(refetchData()) }) diff --git a/front/src/domain/mind/api/mergeTypoRequest.ts b/front/src/domain/mind/api/mergeTypoRequest.ts new file mode 100644 index 00000000..732df5ec --- /dev/null +++ b/front/src/domain/mind/api/mergeTypoRequest.ts @@ -0,0 +1,7 @@ +import { Api } from '@front/domain/api' +import { MergeTypoModel } from '@shared/models/mind/MergeTypoModel' + +export const mergeTypoRequest = (api: Api) => ( + merge: MergeTypoModel, +): Promise => + api.client.post('/mind/typo/merge', merge).then(response => response.data) diff --git a/front/src/features/app/features/now/tips/components/merge/Merge.tsx b/front/src/features/app/features/now/tips/components/merge/Merge.tsx index 24684f62..f3879109 100644 --- a/front/src/features/app/features/now/tips/components/merge/Merge.tsx +++ b/front/src/features/app/features/now/tips/components/merge/Merge.tsx @@ -21,9 +21,12 @@ export const Merge = ({ token, variants, target }: Props) => { const createOnMerge = useCallback( (mainVariant: string) => { - const otherVaraints = variants.filter(varinat => varinat !== mainVariant) + const merge = { + primary: mainVariant, + secondary: variants.filter(varinat => varinat !== mainVariant), + } - return () => dispatch(mergeTypos(token, mainVariant, otherVaraints)) + return () => dispatch(mergeTypos(token, merge)) }, [variants, token], ) diff --git a/shared/models/mind/MergeTypoModel.ts b/shared/models/mind/MergeTypoModel.ts new file mode 100644 index 00000000..c1122c86 --- /dev/null +++ b/shared/models/mind/MergeTypoModel.ts @@ -0,0 +1,4 @@ +export interface MergeTypoModel { + primary: string + secondary: string[] +}