From 3d3ec738043da53efb220452b39165e7ff0a9675 Mon Sep 17 00:00:00 2001 From: Wesley Finck Date: Mon, 23 Feb 2026 15:03:13 -0800 Subject: [PATCH] get collection contributor query --- .../GetCollectionContributorsUseCase.ts | 168 ++++++++++++++++++ .../domain/ICollectionQueryRepository.ts | 12 ++ .../GetCollectionContributorsController.ts | 38 ++++ .../http/routes/collectionRoutes.ts | 9 + .../cards/infrastructure/http/routes/index.ts | 3 + .../DrizzleCollectionQueryRepository.ts | 67 +++++++ .../GetCollectionsForUrlUseCase.test.ts | 1 + .../GetUrlStatusForMyLibraryUseCase.test.ts | 1 + .../InMemoryCollectionQueryRepository.ts | 74 ++++++++ src/shared/infrastructure/http/app.ts | 1 + .../http/factories/ControllerFactory.ts | 6 + .../http/factories/UseCaseFactory.ts | 7 + src/types/src/api/common.ts | 5 + src/types/src/api/requests.ts | 4 + src/types/src/api/responses.ts | 7 + src/webapp/api-client/ApiClient.ts | 8 + src/webapp/api-client/clients/QueryClient.ts | 17 ++ 17 files changed, 428 insertions(+) create mode 100644 src/modules/cards/application/useCases/queries/GetCollectionContributorsUseCase.ts create mode 100644 src/modules/cards/infrastructure/http/controllers/GetCollectionContributorsController.ts diff --git a/src/modules/cards/application/useCases/queries/GetCollectionContributorsUseCase.ts b/src/modules/cards/application/useCases/queries/GetCollectionContributorsUseCase.ts new file mode 100644 index 00000000..f6e82d59 --- /dev/null +++ b/src/modules/cards/application/useCases/queries/GetCollectionContributorsUseCase.ts @@ -0,0 +1,168 @@ +import { err, ok, Result } from 'src/shared/core/Result'; +import { UseCase } from 'src/shared/core/UseCase'; +import { ICollectionQueryRepository } from '../../../domain/ICollectionQueryRepository'; +import { IProfileService } from 'src/modules/cards/domain/services/IProfileService'; +import { ICollectionRepository } from 'src/modules/cards/domain/ICollectionRepository'; +import { CollectionId } from 'src/modules/cards/domain/value-objects/CollectionId'; +import { ContributorUser } from '@semble/types'; +import { ProfileEnricher } from 'src/modules/cards/application/services/ProfileEnricher'; + +export interface GetCollectionContributorsQuery { + collectionId: string; // Collection UUID + callingUserId?: string; + page?: number; + limit?: number; +} + +export interface GetCollectionContributorsResult { + users: ContributorUser[]; + pagination: { + currentPage: number; + totalPages: number; + totalCount: number; + hasMore: boolean; + limit: number; + }; +} + +export class ValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ValidationError'; + } +} + +export class GetCollectionContributorsUseCase + implements + UseCase< + GetCollectionContributorsQuery, + Result + > +{ + constructor( + private collectionQueryRepository: ICollectionQueryRepository, + private profileService: IProfileService, + private collectionRepository: ICollectionRepository, + ) {} + + async execute( + query: GetCollectionContributorsQuery, + ): Promise> { + // Set defaults + const page = query.page || 1; + const limit = Math.min(query.limit || 20, 100); // Cap at 100 + + // Validate collection ID + const collectionIdResult = CollectionId.createFromString( + query.collectionId, + ); + if (collectionIdResult.isErr()) { + return err( + new ValidationError( + `Invalid collection ID: ${collectionIdResult.error.message}`, + ), + ); + } + + try { + // Verify collection exists + const collectionResult = await this.collectionRepository.findById( + collectionIdResult.value, + ); + if (collectionResult.isErr()) { + return err( + new Error( + `Failed to fetch collection: ${collectionResult.error instanceof Error ? collectionResult.error.message : 'Unknown error'}`, + ), + ); + } + if (!collectionResult.value) { + return err(new ValidationError('Collection not found')); + } + + const collection = collectionResult.value; + const authorId = collection.authorId.value; + + // Get contributors from repository (excluding author) + const contributorsResult = + await this.collectionQueryRepository.getCollectionContributors( + query.collectionId, + authorId, + { page, limit }, + ); + + const totalCount = contributorsResult.totalCount; + + // Extract unique contributor IDs + const uniqueContributorIds = contributorsResult.items.map( + (item) => item.userId, + ); + + if (uniqueContributorIds.length === 0) { + return ok({ + users: [], + pagination: { + currentPage: page, + totalPages: 0, + totalCount: 0, + hasMore: false, + limit, + }, + }); + } + + // Fetch profiles for all contributors using ProfileEnricher + const profileEnricher = new ProfileEnricher(this.profileService); + const profileMapResult = await profileEnricher.buildProfileMap( + uniqueContributorIds, + query.callingUserId, + { + skipFailures: false, // Fail if any profile fetch fails + mapToUser: true, // Use full User DTO with isFollowing + }, + ); + + if (profileMapResult.isErr()) { + return err( + new Error( + `Failed to fetch user profiles: ${profileMapResult.error.message}`, + ), + ); + } + + const profileMap = profileMapResult.value; + + // Build users array in the order of contributors (chronological by most recent contribution) + // Add contributionCount to each user + const users: ContributorUser[] = contributorsResult.items + .map((item) => { + const user = profileMap.get(item.userId); + if (!user) { + return undefined; + } + return { + ...user, + contributionCount: item.contributionCount, + }; + }) + .filter((user): user is ContributorUser => user !== undefined); + + return ok({ + users, + pagination: { + currentPage: page, + totalPages: Math.ceil(totalCount / limit), + totalCount, + hasMore: page * limit < totalCount, + limit, + }, + }); + } catch (error) { + return err( + new Error( + `Failed to retrieve collection contributors: ${error instanceof Error ? error.message : 'Unknown error'}`, + ), + ); + } + } +} diff --git a/src/modules/cards/domain/ICollectionQueryRepository.ts b/src/modules/cards/domain/ICollectionQueryRepository.ts index 4cc99ed3..53717c00 100644 --- a/src/modules/cards/domain/ICollectionQueryRepository.ts +++ b/src/modules/cards/domain/ICollectionQueryRepository.ts @@ -93,6 +93,12 @@ export interface GetOpenCollectionsWithContributorOptions { sortOrder: SortOrder; } +export interface CollectionContributorDTO { + userId: string; + contributionCount: number; + lastContributedAt: Date; +} + export interface ICollectionQueryRepository { findByCreator( curatorId: string, @@ -116,4 +122,10 @@ export interface ICollectionQueryRepository { getOpenCollectionsWithContributor( options: GetOpenCollectionsWithContributorOptions, ): Promise>; + + getCollectionContributors( + collectionId: string, + authorId: string, + options: { page: number; limit: number }, + ): Promise>; } diff --git a/src/modules/cards/infrastructure/http/controllers/GetCollectionContributorsController.ts b/src/modules/cards/infrastructure/http/controllers/GetCollectionContributorsController.ts new file mode 100644 index 00000000..d7688f89 --- /dev/null +++ b/src/modules/cards/infrastructure/http/controllers/GetCollectionContributorsController.ts @@ -0,0 +1,38 @@ +import { Controller } from '../../../../../shared/infrastructure/http/Controller'; +import { Request, Response } from 'express'; +import { GetCollectionContributorsUseCase } from '../../../application/useCases/queries/GetCollectionContributorsUseCase'; + +export class GetCollectionContributorsController extends Controller { + constructor( + private getCollectionContributorsUseCase: GetCollectionContributorsUseCase, + ) { + super(); + } + + async executeImpl(req: Request, res: Response): Promise { + try { + const { collectionId } = req.params; + const { page, limit } = req.query; + const callingUserId = (req as any).did; + + if (!collectionId) { + return this.fail(res, 'Collection ID is required'); + } + + const result = await this.getCollectionContributorsUseCase.execute({ + collectionId, + callingUserId, + page: page ? parseInt(page as string) : undefined, + limit: limit ? parseInt(limit as string) : undefined, + }); + + if (result.isErr()) { + return this.fail(res, result.error); + } + + return this.ok(res, result.value); + } catch (error: any) { + return this.fail(res, error); + } + } +} diff --git a/src/modules/cards/infrastructure/http/routes/collectionRoutes.ts b/src/modules/cards/infrastructure/http/routes/collectionRoutes.ts index bb09ba7a..8c1befc6 100644 --- a/src/modules/cards/infrastructure/http/routes/collectionRoutes.ts +++ b/src/modules/cards/infrastructure/http/routes/collectionRoutes.ts @@ -11,6 +11,7 @@ import { SearchCollectionsController } from '../controllers/SearchCollectionsCon import { GetOpenCollectionsWithContributorController } from '../controllers/GetOpenCollectionsWithContributorController'; import { GetCollectionFollowersController } from '../controllers/GetCollectionFollowersController'; import { GetCollectionFollowersCountController } from '../controllers/GetCollectionFollowersCountController'; +import { GetCollectionContributorsController } from '../controllers/GetCollectionContributorsController'; import { AuthMiddleware } from 'src/shared/infrastructure/http/middleware'; export function createCollectionRoutes( @@ -27,6 +28,7 @@ export function createCollectionRoutes( getOpenCollectionsWithContributorController: GetOpenCollectionsWithContributorController, getCollectionFollowersController: GetCollectionFollowersController, getCollectionFollowersCountController: GetCollectionFollowersCountController, + getCollectionContributorsController: GetCollectionContributorsController, ): Router { const router = Router(); @@ -79,6 +81,13 @@ export function createCollectionRoutes( (req, res) => getCollectionFollowersCountController.execute(req, res), ); + // GET /api/collections/:collectionId/contributors - Get collection contributors + router.get( + '/:collectionId/contributors', + authMiddleware.optionalAuth(), + (req, res) => getCollectionContributorsController.execute(req, res), + ); + // GET /api/collections/:collectionId - Get collection page router.get('/:collectionId', authMiddleware.optionalAuth(), (req, res) => getCollectionPageController.execute(req, res), diff --git a/src/modules/cards/infrastructure/http/routes/index.ts b/src/modules/cards/infrastructure/http/routes/index.ts index 2d4649f4..7cb51727 100644 --- a/src/modules/cards/infrastructure/http/routes/index.ts +++ b/src/modules/cards/infrastructure/http/routes/index.ts @@ -29,6 +29,7 @@ import { GetCollectionPageByAtUriController } from '../controllers/GetCollection import { GetCollectionsForUrlController } from '../controllers/GetCollectionsForUrlController'; import { GetCollectionFollowersController } from '../controllers/GetCollectionFollowersController'; import { GetCollectionFollowersCountController } from '../controllers/GetCollectionFollowersCountController'; +import { GetCollectionContributorsController } from '../controllers/GetCollectionContributorsController'; export function createCardsModuleRoutes( authMiddleware: AuthMiddleware, @@ -61,6 +62,7 @@ export function createCardsModuleRoutes( getOpenCollectionsWithContributorController: GetOpenCollectionsWithContributorController, getCollectionFollowersController: GetCollectionFollowersController, getCollectionFollowersCountController: GetCollectionFollowersCountController, + getCollectionContributorsController: GetCollectionContributorsController, ): Router { const router = Router(); @@ -104,6 +106,7 @@ export function createCardsModuleRoutes( getOpenCollectionsWithContributorController, getCollectionFollowersController, getCollectionFollowersCountController, + getCollectionContributorsController, ), ); diff --git a/src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts b/src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts index d68c56a9..3927b80b 100644 --- a/src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts +++ b/src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts @@ -22,6 +22,7 @@ import { CollectionForUrlQueryOptions, SearchCollectionsOptions, GetOpenCollectionsWithContributorOptions, + CollectionContributorDTO, } from '../../domain/ICollectionQueryRepository'; import { collections, collectionCards } from './schema/collection.sql'; import { publishedRecords } from './schema/publishedRecord.sql'; @@ -497,6 +498,72 @@ export class DrizzleCollectionQueryRepository } } + async getCollectionContributors( + collectionId: string, + authorId: string, + options: { page: number; limit: number }, + ): Promise> { + try { + const { page, limit } = options; + const offset = (page - 1) * limit; + + // Get unique contributors with their contribution count and last contribution time + // Exclude the collection author + const contributorsQuery = this.db + .select({ + userId: collectionCards.addedBy, + contributionCount: sql`CAST(COUNT(*) AS INTEGER)`.as( + 'contribution_count', + ), + lastContributedAt: sql`MAX(${collectionCards.addedAt})`.as( + 'last_contributed_at', + ), + }) + .from(collectionCards) + .where( + and( + eq(collectionCards.collectionId, collectionId), + sql`${collectionCards.addedBy} != ${authorId}`, // Exclude author + ), + ) + .groupBy(collectionCards.addedBy) + .orderBy(sql`MAX(${collectionCards.addedAt}) DESC`) // Most recent contribution first + .limit(limit) + .offset(offset); + + const contributors = await contributorsQuery; + + // Get total count of distinct contributors (excluding author) + const countQuery = this.db + .select({ + count: sql`COUNT(DISTINCT ${collectionCards.addedBy})`, + }) + .from(collectionCards) + .where( + and( + eq(collectionCards.collectionId, collectionId), + sql`${collectionCards.addedBy} != ${authorId}`, + ), + ); + + const countResult = await countQuery; + const totalCount = countResult[0]?.count || 0; + + return { + items: contributors.map((c) => ({ + userId: c.userId, + contributionCount: c.contributionCount, + lastContributedAt: c.lastContributedAt, + })), + totalCount, + hasMore: page * limit < totalCount, + }; + } catch (error) { + console.error('Error in getCollectionContributors:', error); + throw error; + } + } + private getSortColumn(sortBy: CollectionSortField) { switch (sortBy) { case CollectionSortField.NAME: diff --git a/src/modules/cards/tests/application/GetCollectionsForUrlUseCase.test.ts b/src/modules/cards/tests/application/GetCollectionsForUrlUseCase.test.ts index 9ad64ab6..8d7a6d53 100644 --- a/src/modules/cards/tests/application/GetCollectionsForUrlUseCase.test.ts +++ b/src/modules/cards/tests/application/GetCollectionsForUrlUseCase.test.ts @@ -785,6 +785,7 @@ describe('GetCollectionsForUrlUseCase', () => { .mockRejectedValue(new Error('Database error')), searchCollections: jest.fn(), getOpenCollectionsWithContributor: jest.fn(), + getCollectionContributors: jest.fn(), }; const errorUseCase = new GetCollectionsForUrlUseCase( diff --git a/src/modules/cards/tests/application/GetUrlStatusForMyLibraryUseCase.test.ts b/src/modules/cards/tests/application/GetUrlStatusForMyLibraryUseCase.test.ts index 046c5009..9d66afaa 100644 --- a/src/modules/cards/tests/application/GetUrlStatusForMyLibraryUseCase.test.ts +++ b/src/modules/cards/tests/application/GetUrlStatusForMyLibraryUseCase.test.ts @@ -597,6 +597,7 @@ describe('GetUrlStatusForMyLibraryUseCase', () => { getCollectionsWithUrl: jest.fn(), searchCollections: jest.fn(), getOpenCollectionsWithContributor: jest.fn(), + getCollectionContributors: jest.fn(), }; const errorUseCase = new GetUrlStatusForMyLibraryUseCase( diff --git a/src/modules/cards/tests/utils/InMemoryCollectionQueryRepository.ts b/src/modules/cards/tests/utils/InMemoryCollectionQueryRepository.ts index 483276d5..e8729596 100644 --- a/src/modules/cards/tests/utils/InMemoryCollectionQueryRepository.ts +++ b/src/modules/cards/tests/utils/InMemoryCollectionQueryRepository.ts @@ -10,6 +10,7 @@ import { CollectionForUrlQueryOptions, SearchCollectionsOptions, GetOpenCollectionsWithContributorOptions, + CollectionContributorDTO, } from '../../domain/ICollectionQueryRepository'; import { Collection } from '../../domain/Collection'; import { InMemoryCollectionRepository } from './InMemoryCollectionRepository'; @@ -397,6 +398,79 @@ export class InMemoryCollectionQueryRepository } } + async getCollectionContributors( + collectionId: string, + authorId: string, + options: { page: number; limit: number }, + ): Promise> { + try { + const allCollections = this.collectionRepository.getAllCollections(); + const collection = allCollections.find( + (c) => c.collectionId.getStringValue() === collectionId, + ); + + if (!collection) { + return { + items: [], + totalCount: 0, + hasMore: false, + }; + } + + // Get unique contributors (excluding author) with their contribution counts + const contributorMap = new Map< + string, + { userId: string; contributionCount: number; lastContributedAt: Date } + >(); + + for (const link of collection.cardLinks) { + const contributorId = link.addedBy.value; + + // Skip the collection author + if (contributorId === authorId) { + continue; + } + + if (contributorMap.has(contributorId)) { + const existing = contributorMap.get(contributorId)!; + existing.contributionCount++; + if (link.addedAt > existing.lastContributedAt) { + existing.lastContributedAt = link.addedAt; + } + } else { + contributorMap.set(contributorId, { + userId: contributorId, + contributionCount: 1, + lastContributedAt: link.addedAt, + }); + } + } + + // Convert to array and sort by most recent contribution + let contributors = Array.from(contributorMap.values()).sort( + (a, b) => b.lastContributedAt.getTime() - a.lastContributedAt.getTime(), + ); + + const totalCount = contributors.length; + + // Apply pagination + const { page, limit } = options; + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + contributors = contributors.slice(startIndex, endIndex); + + return { + items: contributors, + totalCount, + hasMore: endIndex < totalCount, + }; + } catch (error) { + throw new Error( + `Failed to get collection contributors: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + clear(): void { // No separate state to clear } diff --git a/src/shared/infrastructure/http/app.ts b/src/shared/infrastructure/http/app.ts index 076acd3d..7ffc146a 100644 --- a/src/shared/infrastructure/http/app.ts +++ b/src/shared/infrastructure/http/app.ts @@ -126,6 +126,7 @@ export const createExpressApp = ( controllers.getOpenCollectionsWithContributorController, controllers.getCollectionFollowersController, controllers.getCollectionFollowersCountController, + controllers.getCollectionContributorsController, ); const feedRouter = createFeedRoutes( diff --git a/src/shared/infrastructure/http/factories/ControllerFactory.ts b/src/shared/infrastructure/http/factories/ControllerFactory.ts index e7f353cf..d26359bf 100644 --- a/src/shared/infrastructure/http/factories/ControllerFactory.ts +++ b/src/shared/infrastructure/http/factories/ControllerFactory.ts @@ -54,6 +54,7 @@ import { GetFollowersCountController } from '../../../../modules/user/infrastruc import { GetFollowingCollectionsCountController } from '../../../../modules/user/infrastructure/http/controllers/GetFollowingCollectionsCountController'; import { GetCollectionFollowersController } from '../../../../modules/cards/infrastructure/http/controllers/GetCollectionFollowersController'; import { GetCollectionFollowersCountController } from '../../../../modules/cards/infrastructure/http/controllers/GetCollectionFollowersCountController'; +import { GetCollectionContributorsController } from '../../../../modules/cards/infrastructure/http/controllers/GetCollectionContributorsController'; import { CookieService } from '../services/CookieService'; export interface Controllers { @@ -102,6 +103,7 @@ export interface Controllers { getNoteCardsForUrlController: GetNoteCardsForUrlController; getCollectionFollowersController: GetCollectionFollowersController; getCollectionFollowersCountController: GetCollectionFollowersCountController; + getCollectionContributorsController: GetCollectionContributorsController; // Feed controllers getGlobalFeedController: GetGlobalFeedController; getGemActivityFeedController: GetGemActivityFeedController; @@ -265,6 +267,10 @@ export class ControllerFactory { new GetCollectionFollowersCountController( useCases.getCollectionFollowersCountUseCase, ), + getCollectionContributorsController: + new GetCollectionContributorsController( + useCases.getCollectionContributorsUseCase, + ), // Feed controllers getGlobalFeedController: new GetGlobalFeedController( diff --git a/src/shared/infrastructure/http/factories/UseCaseFactory.ts b/src/shared/infrastructure/http/factories/UseCaseFactory.ts index 2c0eafe9..e8b4b1a3 100644 --- a/src/shared/infrastructure/http/factories/UseCaseFactory.ts +++ b/src/shared/infrastructure/http/factories/UseCaseFactory.ts @@ -64,6 +64,7 @@ import { GetFollowingCountUseCase } from '../../../../modules/user/application/u import { GetFollowersCountUseCase } from '../../../../modules/user/application/useCases/queries/GetFollowersCountUseCase'; import { GetFollowingCollectionsCountUseCase } from '../../../../modules/user/application/useCases/queries/GetFollowingCollectionsCountUseCase'; import { GetCollectionFollowersCountUseCase } from '../../../../modules/user/application/useCases/queries/GetCollectionFollowersCountUseCase'; +import { GetCollectionContributorsUseCase } from '../../../../modules/cards/application/useCases/queries/GetCollectionContributorsUseCase'; export interface WorkerUseCases { addActivityToFeedUseCase: AddActivityToFeedUseCase; @@ -106,6 +107,7 @@ export interface UseCases { getFollowersCountUseCase: GetFollowersCountUseCase; getFollowingCollectionsCountUseCase: GetFollowingCollectionsCountUseCase; getCollectionFollowersCountUseCase: GetCollectionFollowersCountUseCase; + getCollectionContributorsUseCase: GetCollectionContributorsUseCase; // Card use cases addUrlToLibraryUseCase: AddUrlToLibraryUseCase; addCardToLibraryUseCase: AddCardToLibraryUseCase; @@ -245,6 +247,11 @@ export class UseCaseFactory { repositories.followsRepository, repositories.collectionRepository, ), + getCollectionContributorsUseCase: new GetCollectionContributorsUseCase( + repositories.collectionQueryRepository, + services.profileService, + repositories.collectionRepository, + ), // Card use cases addUrlToLibraryUseCase: new AddUrlToLibraryUseCase( diff --git a/src/types/src/api/common.ts b/src/types/src/api/common.ts index 71de32d5..db2ed819 100644 --- a/src/types/src/api/common.ts +++ b/src/types/src/api/common.ts @@ -13,6 +13,11 @@ export interface User { followedCollectionsCount?: number; // Number of collections this user follows } +// Extended User interface for contributors with contribution count +export interface ContributorUser extends User { + contributionCount: number; // Number of cards this user contributed to the collection +} + // Type alias for inline profile objects (without isFollowing) // Used for nested author objects in collections, cards, etc. export type UserProfileDTO = Omit; diff --git a/src/types/src/api/requests.ts b/src/types/src/api/requests.ts index 524ba308..e092a6b9 100644 --- a/src/types/src/api/requests.ts +++ b/src/types/src/api/requests.ts @@ -292,3 +292,7 @@ export interface GetFollowingCollectionsCountParams { export interface GetCollectionFollowersCountParams { collectionId: string; // Collection UUID } + +export interface GetCollectionContributorsParams extends PaginationParams { + collectionId: string; // Collection UUID +} diff --git a/src/types/src/api/responses.ts b/src/types/src/api/responses.ts index 25dc3644..dedf23b5 100644 --- a/src/types/src/api/responses.ts +++ b/src/types/src/api/responses.ts @@ -1,5 +1,6 @@ import { User, + ContributorUser, Pagination, CardSorting, CollectionSorting, @@ -401,3 +402,9 @@ export interface GetCollectionFollowersResponse { export interface GetFollowCountResponse { count: number; } + +// Contributor response types +export interface GetCollectionContributorsResponse { + users: ContributorUser[]; + pagination: Pagination; +} diff --git a/src/webapp/api-client/ApiClient.ts b/src/webapp/api-client/ApiClient.ts index dd86e84d..b092dcbb 100644 --- a/src/webapp/api-client/ApiClient.ts +++ b/src/webapp/api-client/ApiClient.ts @@ -98,6 +98,8 @@ import type { GetFollowingCollectionsCountParams, GetCollectionFollowersCountParams, GetFollowCountResponse, + GetCollectionContributorsParams, + GetCollectionContributorsResponse, } from '@semble/types'; // Main API Client class using composition @@ -288,6 +290,12 @@ export class ApiClient { return this.queryClient.getCollectionFollowersCount(params); } + async getCollectionContributors( + params: GetCollectionContributorsParams, + ): Promise { + return this.queryClient.getCollectionContributors(params); + } + // Card operations - delegate to CardClient async addUrlToLibrary( request: AddUrlToLibraryRequest, diff --git a/src/webapp/api-client/clients/QueryClient.ts b/src/webapp/api-client/clients/QueryClient.ts index 76b29159..63e533bc 100644 --- a/src/webapp/api-client/clients/QueryClient.ts +++ b/src/webapp/api-client/clients/QueryClient.ts @@ -46,6 +46,8 @@ import { GetFollowingCollectionsCountParams, GetCollectionFollowersCountParams, GetFollowCountResponse, + GetCollectionContributorsParams, + GetCollectionContributorsResponse, } from '@semble/types'; export class QueryClient extends BaseClient { @@ -451,4 +453,19 @@ export class QueryClient extends BaseClient { `/api/collections/${params.collectionId}/followers/count`, ); } + + async getCollectionContributors( + params: GetCollectionContributorsParams, + ): Promise { + const searchParams = new URLSearchParams(); + if (params.page) searchParams.set('page', params.page.toString()); + if (params.limit) searchParams.set('limit', params.limit.toString()); + + const queryString = searchParams.toString(); + const endpoint = queryString + ? `/api/collections/${params.collectionId}/contributors?${queryString}` + : `/api/collections/${params.collectionId}/contributors`; + + return this.request('GET', endpoint); + } } -- 2.51.2