From 1a035d3f8cd8c86abccc28c0cb96bfb723532fcc Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Fri, 26 Jul 2024 20:07:30 +0900 Subject: [PATCH 1/5] feat: impl fetch list members --- pkg/intermodule/account.ts | 2 +- pkg/timeline/adaptor/controller/timeline.ts | 29 +++++++++ pkg/timeline/adaptor/validator/timeline.ts | 25 ++++++++ pkg/timeline/mod.ts | 16 +++++ pkg/timeline/router.ts | 25 ++++++++ pkg/timeline/service/fetchMember.test.ts | 65 +++++++++++++++++++++ pkg/timeline/service/fetchMember.ts | 29 +++++++++ 7 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 pkg/timeline/service/fetchMember.test.ts create mode 100644 pkg/timeline/service/fetchMember.ts diff --git a/pkg/intermodule/account.ts b/pkg/intermodule/account.ts index a493bfa8..e86817d1 100644 --- a/pkg/intermodule/account.ts +++ b/pkg/intermodule/account.ts @@ -20,7 +20,7 @@ import type { FetchFollowService } from '../accounts/service/fetchFollow.js'; import { fetchFollow } from '../accounts/service/fetchFollow.js'; import { prismaClient } from '../adaptors/prisma.js'; -export type { Account } from '../accounts/model/account.js'; +export { Account } from '../accounts/model/account.js'; export interface PartialAccount { id: AccountID; diff --git a/pkg/timeline/adaptor/controller/timeline.ts b/pkg/timeline/adaptor/controller/timeline.ts index f5df8e65..620c6ed0 100644 --- a/pkg/timeline/adaptor/controller/timeline.ts +++ b/pkg/timeline/adaptor/controller/timeline.ts @@ -8,9 +8,11 @@ import type { ListID } from '../../model/list.js'; import type { AccountTimelineService } from '../../service/account.js'; import type { CreateListService } from '../../service/createList.js'; import type { DeleteListService } from '../../service/deleteList.js'; +import type { FetchListMemberService } from '../../service/fetchMember.js'; import type { CreateListResponseSchema, GetAccountTimelineResponseSchema, + GetListMemberResponseSchema, } from '../validator/timeline.js'; export class TimelineController { @@ -18,16 +20,20 @@ export class TimelineController { private readonly accountModule: AccountModuleFacade; private readonly createListService: CreateListService; private readonly deleteListService: DeleteListService; + private readonly fetchMemberService: FetchListMemberService; + constructor(args: { accountTimelineService: AccountTimelineService; accountModule: AccountModuleFacade; createListService: CreateListService; deleteListService: DeleteListService; + fetchMemberService: FetchListMemberService; }) { this.accountTimelineService = args.accountTimelineService; this.accountModule = args.accountModule; this.createListService = args.createListService; this.deleteListService = args.deleteListService; + this.fetchMemberService = args.fetchMemberService; } async getAccountTimeline( @@ -122,4 +128,27 @@ export class TimelineController { const res = await this.deleteListService.handle(id as ListID); return res; } + + async getListMembers( + id: string, + ): Promise< + Result.Result> + > { + const accounts = await this.fetchMemberService.handle(id as ListID); + if (Result.isErr(accounts)) { + return accounts; + } + + const unwrapped = Result.unwrap(accounts); + const res = unwrapped.map((v) => { + return { + id: v.getID(), + name: v.getName(), + nickname: v.getNickname(), + avatar: '', + }; + }); + + return Result.ok({ assignees: res }); + } } diff --git a/pkg/timeline/adaptor/validator/timeline.ts b/pkg/timeline/adaptor/validator/timeline.ts index 6884b990..fc198a22 100644 --- a/pkg/timeline/adaptor/validator/timeline.ts +++ b/pkg/timeline/adaptor/validator/timeline.ts @@ -67,3 +67,28 @@ export const CreateListResponseSchema = z }), }) .openapi('CreateListResponse'); + +export const GetListMemberResponseSchema = z + .object({ + assignees: z.array( + z.object({ + id: z.string().openapi({ + example: '30984308495', + description: 'Assignee account ID', + }), + name: z.string().openapi({ + example: '@john@example.com', + description: 'Assignee account name', + }), + nickname: z.string().openapi({ + example: 'John Doe', + description: 'Assignee nickname', + }), + avatar: z.string().url().openapi({ + example: 'https://example.com/avatar.png', + description: 'avatar URL', + }), + }), + ), + }) + .openapi('GetListMemberResponseSchema'); diff --git a/pkg/timeline/mod.ts b/pkg/timeline/mod.ts index c1119c28..e3711d52 100644 --- a/pkg/timeline/mod.ts +++ b/pkg/timeline/mod.ts @@ -15,11 +15,13 @@ import { CreateListRoute, DeleteListRoute, GetAccountTimelineRoute, + GetListMemberRoute, PushNoteToTimelineRoute, } from './router.js'; import { AccountTimelineService } from './service/account.js'; import { CreateListService } from './service/createList.js'; import { DeleteListService } from './service/deleteList.js'; +import { FetchListMemberService } from './service/fetchMember.js'; import { NoteVisibilityService } from './service/noteVisibility.js'; import { PushTimelineService } from './service/push.js'; @@ -31,6 +33,7 @@ const timelineRepository = new InMemoryTimelineRepository(); const listRepository = new InMemoryListRepository(); const timelineNotesCacheRepository = new InMemoryTimelineCacheRepository(); const noteVisibilityService = new NoteVisibilityService(accountModule); + const controller = new TimelineController({ accountTimelineService: new AccountTimelineService({ noteVisibilityService: noteVisibilityService, @@ -39,6 +42,7 @@ const controller = new TimelineController({ createListService: new CreateListService(idGenerator, listRepository), deleteListService: new DeleteListService(listRepository), accountModule, + fetchMemberService: new FetchListMemberService(listRepository, accountModule), }); const pushTimelineService = new PushTimelineService( accountModule, @@ -126,3 +130,15 @@ timeline.openapi(DeleteListRoute, async (c) => { return new Response(undefined, { status: 204 }); }); + +timeline.openapi(GetListMemberRoute, async (c) => { + const { id } = c.req.param(); + + const res = await controller.getListMembers(id); + if (Result.isErr(res)) { + return c.json({ error: res[1].message }, 404); + } + + const unwrapped = Result.unwrap(res); + return c.json(unwrapped, 200); +}); diff --git a/pkg/timeline/router.ts b/pkg/timeline/router.ts index d6116b61..4294675a 100644 --- a/pkg/timeline/router.ts +++ b/pkg/timeline/router.ts @@ -5,6 +5,7 @@ import { CreateListRequestSchema, CreateListResponseSchema, GetAccountTimelineResponseSchema, + GetListMemberResponseSchema, } from './adaptor/validator/timeline.js'; export const GetAccountTimelineRoute = createRoute({ @@ -164,3 +165,27 @@ export const DeleteListRoute = createRoute({ }, }, }); + +export const GetListMemberRoute = createRoute({ + method: 'get', + tags: ['timeline'], + path: '/lists/:id/members', + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: GetListMemberResponseSchema, + }, + }, + }, + 404: { + content: { + 'application/json': { + schema: CommonErrorResponseSchema, + }, + }, + description: 'LIST_NOTFOUND', + }, + }, +}); diff --git a/pkg/timeline/service/fetchMember.test.ts b/pkg/timeline/service/fetchMember.test.ts new file mode 100644 index 00000000..e7f5a455 --- /dev/null +++ b/pkg/timeline/service/fetchMember.test.ts @@ -0,0 +1,65 @@ +import { Result } from '@mikuroxina/mini-fn'; +import { describe, expect, it, vi } from 'vitest'; +import type { AccountID } from '../../accounts/model/account.js'; +import { + Account, + dummyAccountModuleFacade, +} from '../../intermodule/account.js'; +import { InMemoryListRepository } from '../adaptor/repository/dummy.js'; +import { List, type ListID } from '../model/list.js'; +import { FetchListMemberService } from './fetchMember.js'; + +describe('FetchListMemberService', () => { + const dummyListData = List.new({ + id: '1' as ListID, + title: 'List', + publicity: 'PRIVATE', + memberIds: ['11' as AccountID, '12' as AccountID], + ownerId: '1' as AccountID, + createdAt: new Date('2024-01-01T00:00:00.000Z'), + }); + const repository = new InMemoryListRepository([dummyListData]); + const dummyAccountData = [ + Account.new({ + id: '11' as AccountID, + name: '@johndoe@example.com', + nickname: 'John Doe', + bio: '', + mail: '', + role: 'normal', + status: 'active', + silenced: 'normal', + frozen: 'normal', + createdAt: new Date('2023-09-10T00:00:00.000Z'), + }), + Account.new({ + id: '12' as AccountID, + name: '@test@example.com', + nickname: 'Test User', + bio: '', + mail: '', + role: 'normal', + status: 'active', + silenced: 'normal', + frozen: 'normal', + createdAt: new Date('2023-09-11T00:00:00.000Z'), + }), + ]; + const service = new FetchListMemberService( + repository, + dummyAccountModuleFacade, + ); + + it('should fetch list members', async () => { + vi.spyOn(dummyAccountModuleFacade, 'fetchAccounts').mockImplementation( + async () => { + return Result.ok(dummyAccountData); + }, + ); + + const res = await service.handle('1' as ListID); + + expect(Result.isOk(res)).toBe(true); + expect(Result.unwrap(res)).toStrictEqual(dummyAccountData); + }); +}); diff --git a/pkg/timeline/service/fetchMember.ts b/pkg/timeline/service/fetchMember.ts new file mode 100644 index 00000000..079f7057 --- /dev/null +++ b/pkg/timeline/service/fetchMember.ts @@ -0,0 +1,29 @@ +import { Result } from '@mikuroxina/mini-fn'; +import type { + Account, + AccountModuleFacade, +} from '../../intermodule/account.js'; +import type { ListID } from '../model/list.js'; +import type { ListRepository } from '../model/repository.js'; + +export class FetchListMemberService { + constructor( + private readonly listRepository: ListRepository, + private readonly accountModule: AccountModuleFacade, + ) {} + + async handle(listID: ListID): Promise> { + const list = await this.listRepository.fetchListMembers(listID); + if (Result.isErr(list)) { + return list; + } + const unwrappedAccountID = Result.unwrap(list); + + const accounts = await this.accountModule.fetchAccounts(unwrappedAccountID); + if (Result.isErr(accounts)) { + return accounts; + } + + return Result.ok(Result.unwrap(accounts)); + } +} From c19fa81ff891e64d66e9d5dcb6f144860cd6d9dc Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Fri, 26 Jul 2024 20:21:13 +0900 Subject: [PATCH 2/5] chore(docs): update api references --- resources/schema.json | 83 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/resources/schema.json b/resources/schema.json index 31ce2381..6719499e 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -483,6 +483,49 @@ }, "List ID": { "type": "string" + }, + "GetListMemberResponseSchema": { + "type": "object", + "properties": { + "assignees": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Assignee account ID", + "example": "30984308495" + }, + "name": { + "type": "string", + "description": "Assignee account name", + "example": "@john@example.com" + }, + "nickname": { + "type": "string", + "description": "Assignee nickname", + "example": "John Doe" + }, + "avatar": { + "type": "string", + "format": "uri", + "description": "avatar URL", + "example": "https://example.com/avatar.png" + } + }, + "required": [ + "id", + "name", + "nickname", + "avatar" + ] + } + } + }, + "required": [ + "assignees" + ] } }, "parameters": {} @@ -2565,6 +2608,46 @@ } } } + }, + "/lists/:id/members": { + "get": { + "tags": [ + "timeline" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetListMemberResponseSchema" + } + } + } + }, + "404": { + "description": "LIST_NOTFOUND", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "default": "", + "description": "Error code", + "example": "TEST_ERROR_CODE" + } + }, + "required": [ + "error" + ] + } + } + } + } + } + } } } } \ No newline at end of file From f12256223698f32c7bea91a75eb75a9ab9c7ee5c Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Fri, 26 Jul 2024 23:53:51 +0900 Subject: [PATCH 3/5] refactor: use type export, setup testdata --- pkg/accounts/testData/testData.ts | 44 ++++++++++++++++++++++++ pkg/intermodule/account.ts | 2 +- pkg/timeline/service/fetchMember.test.ts | 36 +++---------------- pkg/timeline/testData/testData.ts | 22 +----------- 4 files changed, 50 insertions(+), 54 deletions(-) create mode 100644 pkg/accounts/testData/testData.ts diff --git a/pkg/accounts/testData/testData.ts b/pkg/accounts/testData/testData.ts new file mode 100644 index 00000000..6962fa61 --- /dev/null +++ b/pkg/accounts/testData/testData.ts @@ -0,0 +1,44 @@ +import type { PartialAccount } from '../../intermodule/account.js'; +import { Account, type AccountID } from '../model/account.js'; + +export const dummyAccount1 = Account.new({ + id: '101' as AccountID, + bio: 'this is test user', + mail: 'john@example.com', + name: '@john@example.com', + nickname: 'John Doe', + passphraseHash: '', + role: 'normal', + silenced: 'normal', + status: 'active', + frozen: 'normal', + createdAt: new Date('2023-09-10T00:00:00Z'), +}); +export const dummyAccount2 = Account.new({ + id: '102' as AccountID, + bio: 'Hello world ✨', + mail: 'john@example.com', + name: '@johndoe@example.com', + nickname: '🌤 John', + passphraseHash: '', + role: 'normal', + silenced: 'normal', + status: 'active', + frozen: 'normal', + createdAt: new Date('2023-09-11T00:00:00Z'), +}); +export const dummyAccounts = [dummyAccount1, dummyAccount2]; + +export const partialAccount1: PartialAccount = { + id: dummyAccount1.getID(), + name: dummyAccount1.getName(), + nickname: dummyAccount1.getNickname(), + bio: dummyAccount1.getBio(), +}; +export const partialAccount2: PartialAccount = { + id: dummyAccount2.getID(), + name: dummyAccount2.getName(), + nickname: dummyAccount2.getNickname(), + bio: dummyAccount2.getBio(), +}; +export const partialAccounts = [partialAccount1, partialAccount2]; diff --git a/pkg/intermodule/account.ts b/pkg/intermodule/account.ts index e86817d1..a493bfa8 100644 --- a/pkg/intermodule/account.ts +++ b/pkg/intermodule/account.ts @@ -20,7 +20,7 @@ import type { FetchFollowService } from '../accounts/service/fetchFollow.js'; import { fetchFollow } from '../accounts/service/fetchFollow.js'; import { prismaClient } from '../adaptors/prisma.js'; -export { Account } from '../accounts/model/account.js'; +export type { Account } from '../accounts/model/account.js'; export interface PartialAccount { id: AccountID; diff --git a/pkg/timeline/service/fetchMember.test.ts b/pkg/timeline/service/fetchMember.test.ts index e7f5a455..28f757ad 100644 --- a/pkg/timeline/service/fetchMember.test.ts +++ b/pkg/timeline/service/fetchMember.test.ts @@ -1,10 +1,8 @@ import { Result } from '@mikuroxina/mini-fn'; import { describe, expect, it, vi } from 'vitest'; import type { AccountID } from '../../accounts/model/account.js'; -import { - Account, - dummyAccountModuleFacade, -} from '../../intermodule/account.js'; +import { dummyAccounts } from '../../accounts/testData/testData.js'; +import { dummyAccountModuleFacade } from '../../intermodule/account.js'; import { InMemoryListRepository } from '../adaptor/repository/dummy.js'; import { List, type ListID } from '../model/list.js'; import { FetchListMemberService } from './fetchMember.js'; @@ -19,32 +17,6 @@ describe('FetchListMemberService', () => { createdAt: new Date('2024-01-01T00:00:00.000Z'), }); const repository = new InMemoryListRepository([dummyListData]); - const dummyAccountData = [ - Account.new({ - id: '11' as AccountID, - name: '@johndoe@example.com', - nickname: 'John Doe', - bio: '', - mail: '', - role: 'normal', - status: 'active', - silenced: 'normal', - frozen: 'normal', - createdAt: new Date('2023-09-10T00:00:00.000Z'), - }), - Account.new({ - id: '12' as AccountID, - name: '@test@example.com', - nickname: 'Test User', - bio: '', - mail: '', - role: 'normal', - status: 'active', - silenced: 'normal', - frozen: 'normal', - createdAt: new Date('2023-09-11T00:00:00.000Z'), - }), - ]; const service = new FetchListMemberService( repository, dummyAccountModuleFacade, @@ -53,13 +25,13 @@ describe('FetchListMemberService', () => { it('should fetch list members', async () => { vi.spyOn(dummyAccountModuleFacade, 'fetchAccounts').mockImplementation( async () => { - return Result.ok(dummyAccountData); + return Result.ok(dummyAccounts); }, ); const res = await service.handle('1' as ListID); expect(Result.isOk(res)).toBe(true); - expect(Result.unwrap(res)).toStrictEqual(dummyAccountData); + expect(Result.unwrap(res)).toStrictEqual(dummyAccounts); }); }); diff --git a/pkg/timeline/testData/testData.ts b/pkg/timeline/testData/testData.ts index 3fb81374..6b4fb7c5 100644 --- a/pkg/timeline/testData/testData.ts +++ b/pkg/timeline/testData/testData.ts @@ -3,8 +3,7 @@ * */ import { Option } from '@mikuroxina/mini-fn'; -import { Account, type AccountID } from '../../accounts/model/account.js'; -import type { PartialAccount } from '../../intermodule/account.js'; +import type { AccountID } from '../../accounts/model/account.js'; import { Note, type NoteID } from '../../notes/model/note.js'; export const dummyPublicNote = Note.new({ @@ -51,22 +50,3 @@ export const dummyDirectNote = Note.new({ visibility: 'DIRECT', createdAt: new Date('2023/10/10 00:00:00'), }); -export const dummyAccount1 = Account.new({ - id: '101' as AccountID, - bio: 'this is test user', - mail: 'john@example.com', - name: '@john@example.com', - nickname: 'John Doe', - passphraseHash: '', - role: 'normal', - silenced: 'normal', - status: 'active', - frozen: 'normal', - createdAt: new Date(), -}); -export const partialAccount1: PartialAccount = { - id: dummyAccount1.getID(), - name: dummyAccount1.getName(), - nickname: dummyAccount1.getNickname(), - bio: dummyAccount1.getBio(), -}; From ab5d965e981724f66c64cca123a2e304f0597cde Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Fri, 26 Jul 2024 23:58:06 +0900 Subject: [PATCH 4/5] fix: type errors --- pkg/timeline/service/noteVisibility.test.ts | 2 +- pkg/timeline/service/push.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/timeline/service/noteVisibility.test.ts b/pkg/timeline/service/noteVisibility.test.ts index 7bdbb4c2..64284803 100644 --- a/pkg/timeline/service/noteVisibility.test.ts +++ b/pkg/timeline/service/noteVisibility.test.ts @@ -2,13 +2,13 @@ import { Result } from '@mikuroxina/mini-fn'; import { describe, expect, it, vi } from 'vitest'; import type { AccountID } from '../../accounts/model/account.js'; +import { partialAccount1 } from '../../accounts/testData/testData.js'; import { dummyAccountModuleFacade } from '../../intermodule/account.js'; import { dummyDirectNote, dummyFollowersNote, dummyHomeNote, dummyPublicNote, - partialAccount1, } from '../testData/testData.js'; import { NoteVisibilityService } from './noteVisibility.js'; diff --git a/pkg/timeline/service/push.test.ts b/pkg/timeline/service/push.test.ts index d337051e..912adf6a 100644 --- a/pkg/timeline/service/push.test.ts +++ b/pkg/timeline/service/push.test.ts @@ -1,9 +1,10 @@ import { Result } from '@mikuroxina/mini-fn'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { partialAccount1 } from '../../accounts/testData/testData.js'; import { dummyAccountModuleFacade } from '../../intermodule/account.js'; import { InMemoryTimelineCacheRepository } from '../adaptor/repository/dummyCache.js'; -import { dummyPublicNote, partialAccount1 } from '../testData/testData.js'; +import { dummyPublicNote } from '../testData/testData.js'; import { NoteVisibilityService } from './noteVisibility.js'; import { PushTimelineService } from './push.js'; From 275127715437f1ce68f8e347bfe96abd2c35dd1a Mon Sep 17 00:00:00 2001 From: Tatsuto YAMAMOTO Date: Sat, 27 Jul 2024 11:19:27 +0900 Subject: [PATCH 5/5] fix: use direct return, add ToDo comment --- pkg/timeline/adaptor/controller/timeline.ts | 1 + pkg/timeline/service/fetchMember.ts | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pkg/timeline/adaptor/controller/timeline.ts b/pkg/timeline/adaptor/controller/timeline.ts index 620c6ed0..b66a57a5 100644 --- a/pkg/timeline/adaptor/controller/timeline.ts +++ b/pkg/timeline/adaptor/controller/timeline.ts @@ -145,6 +145,7 @@ export class TimelineController { id: v.getID(), name: v.getName(), nickname: v.getNickname(), + // ToDo: fill avatar URL avatar: '', }; }); diff --git a/pkg/timeline/service/fetchMember.ts b/pkg/timeline/service/fetchMember.ts index 079f7057..ce9a720d 100644 --- a/pkg/timeline/service/fetchMember.ts +++ b/pkg/timeline/service/fetchMember.ts @@ -19,11 +19,6 @@ export class FetchListMemberService { } const unwrappedAccountID = Result.unwrap(list); - const accounts = await this.accountModule.fetchAccounts(unwrappedAccountID); - if (Result.isErr(accounts)) { - return accounts; - } - - return Result.ok(Result.unwrap(accounts)); + return await this.accountModule.fetchAccounts(unwrappedAccountID); } }