From 750d2626041b355459265a4a4148d08f6ac517fd Mon Sep 17 00:00:00 2001 From: okayurisotto <47853651+okayurisotto@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:31:50 +0900 Subject: [PATCH] refactor(backend): `ReactionService.prototype.convertLegacyReactions` (#13375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add unit tests * cleanup unnecessary type assertions * `convertedReaction`変数の定義と変換表に対する存在確認処理の整理 * `count`変数の定義とループ処理での`Object.entries()`の活用 * 条件式の整理 * `Array.prototype.reduce`を使うように * `Array.prototype.reduce`を使うように * 配列操作を1つのメソッドチェーンに整理 これまでの実装では、`decodeReaction`の返り値が同一になる異なる入力値が同時に複数個存在した場合、後ろのもので上書きされてしまっていたはず。 これからの実装では、後ろのものは前のものに加算される。 (実際にこの挙動の変更が問題になるシチュエーションはまずないはず。) * add unit test * ドキュメントコメントの追加と型定義の調整 --- packages/backend/src/core/ReactionService.ts | 49 ++++++++++--------- packages/backend/test/unit/ReactionService.ts | 41 ++++++++++++++++ 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 5014156a5c..cb0b079df0 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -322,35 +322,36 @@ export class ReactionService { //#endregion } + /** + * 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、 + * データベース上には存在する「0個のリアクションがついている」という情報を削除する。 + */ @bindThis - public convertLegacyReactions(reactions: Record) { - const _reactions = {} as Record; + public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] { + return Object.entries(reactions) + .filter(([, count]) => { + // `ReactionService.prototype.delete`ではリアクション削除時に、 + // `MiNote['reactions']`のエントリの値をデクリメントしているが、 + // デクリメントしているだけなのでエントリ自体は0を値として持つ形で残り続ける。 + // そのため、この処理がなければ、「0個のリアクションがついている」ということになってしまう。 + return count > 0; + }) + .map(([reaction, count]) => { + // unchecked indexed access + const convertedReaction = legacies[reaction] as string | undefined; - for (const reaction of Object.keys(reactions)) { - if (reactions[reaction] <= 0) continue; + const key = this.decodeReaction(convertedReaction ?? reaction).reaction; - if (Object.keys(legacies).includes(reaction)) { - if (_reactions[legacies[reaction]]) { - _reactions[legacies[reaction]] += reactions[reaction]; - } else { - _reactions[legacies[reaction]] = reactions[reaction]; - } - } else { - if (_reactions[reaction]) { - _reactions[reaction] += reactions[reaction]; - } else { - _reactions[reaction] = reactions[reaction]; - } - } - } + return [key, count] as const; + }) + .reduce((acc, [key, count]) => { + // unchecked indexed access + const prevCount = acc[key] as number | undefined; - const _reactions2 = {} as Record; + acc[key] = (prevCount ?? 0) + count; - for (const reaction of Object.keys(_reactions)) { - _reactions2[this.decodeReaction(reaction).reaction] = _reactions[reaction]; - } - - return _reactions2; + return acc; + }, {}); } @bindThis diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index d1c31cac3a..1957f4544c 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -90,4 +90,45 @@ describe('ReactionService', () => { assert.strictEqual(await reactionService.normalize('unknown'), '❤'); }); }); + + describe('convertLegacyReactions', () => { + test('空の入力に対しては何もしない', () => { + const input = {}; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); + }); + + test('Unicode絵文字リアクションを変換してしまわない', () => { + const input = { '👍': 1, '🍮': 2 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); + }); + + test('カスタム絵文字リアクションを変換してしまわない', () => { + const input = { ':like@.:': 1, ':pudding@example.tld:': 2 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); + }); + + test('文字列によるレガシーなリアクションを変換する', () => { + const input = { 'like': 1, 'pudding': 2 }; + const output = { '👍': 1, '🍮': 2 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); + }); + + test('host部分が省略されたレガシーなカスタム絵文字リアクションを変換する', () => { + const input = { ':custom_emoji:': 1 }; + const output = { ':custom_emoji@.:': 1 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); + }); + + test('「0個のリアクション」情報を削除する', () => { + const input = { 'angry': 0 }; + const output = {}; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); + }); + + test('host部分の有無によりデコードすると同じ表記になるカスタム絵文字リアクションの個数情報を正しく足し合わせる', () => { + const input = { ':custom_emoji:': 1, ':custom_emoji@.:': 2 }; + const output = { ':custom_emoji@.:': 3 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); + }); + }); });