diff --git a/src/modules/cards/application/useCases/queries/SearchCollectionsUseCase.ts b/src/modules/cards/application/useCases/queries/SearchCollectionsUseCase.ts index 5045dfce..34e4431c 100644 --- a/src/modules/cards/application/useCases/queries/SearchCollectionsUseCase.ts +++ b/src/modules/cards/application/useCases/queries/SearchCollectionsUseCase.ts @@ -11,6 +11,9 @@ import { PaginationDTO, CollectionSortingDTO, } from '@semble/types'; +import { IIdentityResolutionService } from 'src/modules/atproto/domain/services/IIdentityResolutionService'; +import { DIDOrHandle } from 'src/modules/atproto/domain/DIDOrHandle'; +import { CollectionAccessType } from '../../../domain/Collection'; export interface SearchCollectionsQuery { page?: number; @@ -18,6 +21,8 @@ export interface SearchCollectionsQuery { sortBy?: CollectionSortField; sortOrder?: SortOrder; searchText?: string; + identifier?: string; // Can be DID or handle + accessType?: CollectionAccessType; } export interface SearchCollectionsResult { @@ -32,6 +37,7 @@ export class SearchCollectionsUseCase constructor( private collectionQueryRepo: ICollectionQueryRepository, private profileService: IProfileService, + private identityResolutionService: IIdentityResolutionService, ) {} async execute( @@ -44,6 +50,38 @@ export class SearchCollectionsUseCase const sortOrder = query.sortOrder || SortOrder.DESC; try { + // Resolve identifier to DID if provided + let authorId: string | undefined; + if (query.identifier) { + const identifierResult = DIDOrHandle.create(query.identifier); + if (identifierResult.isErr()) { + return err( + new Error(`Invalid identifier: ${identifierResult.error.message}`), + ); + } + + const didResult = await this.identityResolutionService.resolveToDID( + identifierResult.value, + ); + if (didResult.isErr()) { + return err( + new Error( + `Failed to resolve identifier to DID: ${didResult.error.message}`, + ), + ); + } + + authorId = didResult.value.value; + } + + // Validate accessType if provided + if ( + query.accessType && + !Object.values(CollectionAccessType).includes(query.accessType) + ) { + return err(new Error(`Invalid access type: ${query.accessType}`)); + } + // Execute query to get raw collection data const result = await this.collectionQueryRepo.searchCollections({ page, @@ -51,6 +89,8 @@ export class SearchCollectionsUseCase sortBy, sortOrder, searchText: query.searchText, + authorId, + accessType: query.accessType, }); // Get unique author IDs from the results diff --git a/src/modules/cards/domain/ICollectionQueryRepository.ts b/src/modules/cards/domain/ICollectionQueryRepository.ts index 59a4e693..4b917599 100644 --- a/src/modules/cards/domain/ICollectionQueryRepository.ts +++ b/src/modules/cards/domain/ICollectionQueryRepository.ts @@ -78,6 +78,8 @@ export interface SearchCollectionsOptions { sortBy: CollectionSortField; sortOrder: SortOrder; searchText?: string; + authorId?: string; // Filter by author DID + accessType?: string; // Filter by access type (OPEN or CLOSED) } export interface ICollectionQueryRepository { diff --git a/src/modules/cards/infrastructure/http/controllers/SearchCollectionsController.ts b/src/modules/cards/infrastructure/http/controllers/SearchCollectionsController.ts index 3db93488..3e3762a3 100644 --- a/src/modules/cards/infrastructure/http/controllers/SearchCollectionsController.ts +++ b/src/modules/cards/infrastructure/http/controllers/SearchCollectionsController.ts @@ -5,6 +5,7 @@ import { CollectionSortField, SortOrder, } from '../../../domain/ICollectionQueryRepository'; +import { CollectionAccessType } from '../../../domain/Collection'; export class SearchCollectionsController extends Controller { constructor(private searchCollectionsUseCase: SearchCollectionsUseCase) { @@ -13,7 +14,15 @@ export class SearchCollectionsController extends Controller { async executeImpl(req: Request, res: Response): Promise { try { - const { page, limit, sortBy, sortOrder, searchText } = req.query; + const { + page, + limit, + sortBy, + sortOrder, + searchText, + identifier, + accessType, + } = req.query; const result = await this.searchCollectionsUseCase.execute({ page: page ? parseInt(page as string) : undefined, @@ -21,6 +30,8 @@ export class SearchCollectionsController extends Controller { sortBy: sortBy as CollectionSortField, sortOrder: sortOrder as SortOrder, searchText: searchText as string, + identifier: identifier as string, + accessType: accessType as CollectionAccessType, }); if (result.isErr()) { diff --git a/src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts b/src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts index 081ac1ae..6f42e38d 100644 --- a/src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts +++ b/src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts @@ -264,7 +264,15 @@ export class DrizzleCollectionQueryRepository options: SearchCollectionsOptions, ): Promise> { try { - const { page, limit, sortBy, sortOrder, searchText } = options; + const { + page, + limit, + sortBy, + sortOrder, + searchText, + authorId, + accessType, + } = options; const offset = (page - 1) * limit; // Build the sort order @@ -273,6 +281,16 @@ export class DrizzleCollectionQueryRepository // Build where conditions const whereConditions = []; + // Add author filter if provided + if (authorId) { + whereConditions.push(eq(collections.authorId, authorId)); + } + + // Add access type filter if provided + if (accessType) { + whereConditions.push(eq(collections.accessType, accessType)); + } + // Add tokenized search condition if searchText is provided if (searchText && searchText.trim()) { const searchWords = searchText.trim().split(/\s+/); diff --git a/src/modules/cards/tests/utils/InMemoryCollectionQueryRepository.ts b/src/modules/cards/tests/utils/InMemoryCollectionQueryRepository.ts index a1de39c9..d909101b 100644 --- a/src/modules/cards/tests/utils/InMemoryCollectionQueryRepository.ts +++ b/src/modules/cards/tests/utils/InMemoryCollectionQueryRepository.ts @@ -228,6 +228,20 @@ export class InMemoryCollectionQueryRepository try { let allCollections = this.collectionRepository.getAllCollections(); + // Apply author filter if provided + if (options.authorId) { + allCollections = allCollections.filter( + (collection) => collection.authorId.value === options.authorId, + ); + } + + // Apply access type filter if provided + if (options.accessType) { + allCollections = allCollections.filter( + (collection) => collection.accessType === options.accessType, + ); + } + // Apply tokenized search if searchText is provided if (options.searchText && options.searchText.trim()) { const searchWords = options.searchText diff --git a/src/shared/infrastructure/http/factories/UseCaseFactory.ts b/src/shared/infrastructure/http/factories/UseCaseFactory.ts index 4c7f2ed5..306c7122 100644 --- a/src/shared/infrastructure/http/factories/UseCaseFactory.ts +++ b/src/shared/infrastructure/http/factories/UseCaseFactory.ts @@ -238,6 +238,7 @@ export class UseCaseFactory { searchCollectionsUseCase: new SearchCollectionsUseCase( repositories.collectionQueryRepository, services.profileService, + services.identityResolutionService, ), getUrlStatusForMyLibraryUseCase: new GetUrlStatusForMyLibraryUseCase( repositories.cardRepository, diff --git a/src/types/src/api/requests.ts b/src/types/src/api/requests.ts index d9c086aa..aed434a0 100644 --- a/src/types/src/api/requests.ts +++ b/src/types/src/api/requests.ts @@ -208,6 +208,8 @@ export interface GetGemActivityFeedParams extends PaginationParams { export interface SearchCollectionsParams extends PaginatedSortedParams { searchText?: string; + identifier?: string; // Can be DID or handle + accessType?: 'OPEN' | 'CLOSED'; } // Notification request types diff --git a/src/webapp/api-client/clients/CollectionClient.ts b/src/webapp/api-client/clients/CollectionClient.ts index f5261fd5..7ee186f0 100644 --- a/src/webapp/api-client/clients/CollectionClient.ts +++ b/src/webapp/api-client/clients/CollectionClient.ts @@ -50,6 +50,8 @@ export class CollectionClient extends BaseClient { if (params?.sortBy) searchParams.set('sortBy', params.sortBy); if (params?.sortOrder) searchParams.set('sortOrder', params.sortOrder); if (params?.searchText) searchParams.set('searchText', params.searchText); + if (params?.identifier) searchParams.set('identifier', params.identifier); + if (params?.accessType) searchParams.set('accessType', params.accessType); const queryString = searchParams.toString(); const endpoint = queryString