diff --git a/src/webapp/api-client/ApiClient.ts b/src/webapp/api-client/ApiClient.ts --- a/src/webapp/api-client/ApiClient.ts +++ b/src/webapp/api-client/ApiClient.ts @@ -108,6 +108,8 @@ UpdateConnectionResponse, DeleteConnectionRequest, DeleteConnectionResponse, + GetConnectionsParams, + GetConnectionsResponse, GetForwardConnectionsForUrlParams, GetForwardConnectionsForUrlResponse, GetBackwardConnectionsForUrlParams, @@ -470,6 +472,12 @@ } // Connection operations + async getConnections( + params: GetConnectionsParams, + ): Promise { + return this.queryClient.getConnections(params); + } + async createConnection( request: CreateConnectionRequest, ): Promise { diff --git a/src/shared/infrastructure/http/app.ts b/src/shared/infrastructure/http/app.ts --- a/src/shared/infrastructure/http/app.ts +++ b/src/shared/infrastructure/http/app.ts @@ -136,6 +136,7 @@ controllers.createConnectionController, controllers.updateConnectionController, controllers.deleteConnectionController, + controllers.getConnectionsController, controllers.getForwardConnectionsForUrlController, controllers.getBackwardConnectionsForUrlController, ); diff --git a/src/types/src/api/requests.ts b/src/types/src/api/requests.ts --- a/src/types/src/api/requests.ts +++ b/src/types/src/api/requests.ts @@ -334,3 +334,9 @@ searchQuery: string; urlType?: UrlType; } + +// Get connections request types +export interface GetConnectionsParams extends PaginatedSortedParams { + identifier: string; // Can be DID or handle + connectionTypes?: ('CITATION' | 'REFERENCE' | 'RELATED' | 'INSPIRED_BY')[]; +} diff --git a/src/types/src/api/responses.ts b/src/types/src/api/responses.ts --- a/src/types/src/api/responses.ts +++ b/src/types/src/api/responses.ts @@ -449,3 +449,22 @@ pagination: Pagination; sorting: CardSorting; } + +// Get connections response types +export interface ConnectionWithSourceAndTarget { + connection: { + id: string; + type?: string; + note?: string; + createdAt: string; + updatedAt: string; + }; + source: UrlView; + target: UrlView; +} + +export interface GetConnectionsResponse { + connections: ConnectionWithSourceAndTarget[]; + pagination: Pagination; + sorting: ConnectionSorting; +} diff --git a/src/webapp/api-client/clients/QueryClient.ts b/src/webapp/api-client/clients/QueryClient.ts --- a/src/webapp/api-client/clients/QueryClient.ts +++ b/src/webapp/api-client/clients/QueryClient.ts @@ -48,6 +48,8 @@ GetFollowCountResponse, GetCollectionContributorsParams, GetCollectionContributorsResponse, + GetConnectionsParams, + GetConnectionsResponse, GetForwardConnectionsForUrlParams, GetForwardConnectionsForUrlResponse, GetBackwardConnectionsForUrlParams, @@ -511,6 +513,26 @@ 'GET', `/api/connections/backward?${searchParams}`, ); + } + + async getConnections( + params: GetConnectionsParams, + ): Promise { + const searchParams = new URLSearchParams(); + if (params.page) searchParams.set('page', params.page.toString()); + if (params.limit) searchParams.set('limit', params.limit.toString()); + if (params.sortBy) searchParams.set('sortBy', params.sortBy); + if (params.sortOrder) searchParams.set('sortOrder', params.sortOrder); + if (params.connectionTypes) { + searchParams.set('connectionTypes', params.connectionTypes.join(',')); + } + + const queryString = searchParams.toString(); + const endpoint = queryString + ? `/api/connections/user/${params.identifier}?${queryString}` + : `/api/connections/user/${params.identifier}`; + + return this.request('GET', endpoint); } async searchUrls(params: SearchUrlsParams): Promise { diff --git a/src/shared/infrastructure/http/factories/ControllerFactory.ts b/src/shared/infrastructure/http/factories/ControllerFactory.ts --- a/src/shared/infrastructure/http/factories/ControllerFactory.ts +++ b/src/shared/infrastructure/http/factories/ControllerFactory.ts @@ -58,6 +58,7 @@ import { CreateConnectionController } from '../../../../modules/cards/infrastructure/http/controllers/CreateConnectionController'; import { UpdateConnectionController } from '../../../../modules/cards/infrastructure/http/controllers/UpdateConnectionController'; import { DeleteConnectionController } from '../../../../modules/cards/infrastructure/http/controllers/DeleteConnectionController'; +import { GetConnectionsController } from '../../../../modules/cards/infrastructure/http/controllers/GetConnectionsController'; import { GetForwardConnectionsForUrlController } from '../../../../modules/cards/infrastructure/http/controllers/GetForwardConnectionsForUrlController'; import { GetBackwardConnectionsForUrlController } from '../../../../modules/cards/infrastructure/http/controllers/GetBackwardConnectionsForUrlController'; import { SearchUrlsController } from '../../../../modules/cards/infrastructure/http/controllers/SearchUrlsController'; @@ -111,6 +112,7 @@ getCollectionFollowersCountController: GetCollectionFollowersCountController; getCollectionContributorsController: GetCollectionContributorsController; // Connection controllers + getConnectionsController: GetConnectionsController; createConnectionController: CreateConnectionController; updateConnectionController: UpdateConnectionController; deleteConnectionController: DeleteConnectionController; @@ -287,6 +289,9 @@ ), // Connection controllers + getConnectionsController: new GetConnectionsController( + useCases.getConnectionsUseCase, + ), createConnectionController: new CreateConnectionController( useCases.createConnectionUseCase, ), diff --git a/src/shared/infrastructure/http/factories/UseCaseFactory.ts b/src/shared/infrastructure/http/factories/UseCaseFactory.ts --- a/src/shared/infrastructure/http/factories/UseCaseFactory.ts +++ b/src/shared/infrastructure/http/factories/UseCaseFactory.ts @@ -36,6 +36,7 @@ import { GetNoteCardsForUrlUseCase } from '../../../../modules/cards/application/useCases/queries/GetNoteCardsForUrlUseCase'; import { GetForwardConnectionsForUrlUseCase } from '../../../../modules/cards/application/useCases/queries/GetForwardConnectionsForUrlUseCase'; import { GetBackwardConnectionsForUrlUseCase } from '../../../../modules/cards/application/useCases/queries/GetBackwardConnectionsForUrlUseCase'; +import { GetConnectionsUseCase } from '../../../../modules/cards/application/useCases/queries/GetConnectionsUseCase'; import { IndexUrlForSearchUseCase } from '../../../../modules/search/application/useCases/commands/IndexUrlForSearchUseCase'; import { GetSimilarUrlsForUrlUseCase } from '../../../../modules/search/application/useCases/queries/GetSimilarUrlsForUrlUseCase'; import { SemanticSearchUrlsUseCase } from '../../../../modules/search/application/useCases/queries/SemanticSearchUrlsUseCase'; @@ -143,6 +144,7 @@ getForwardConnectionsForUrlUseCase: GetForwardConnectionsForUrlUseCase; getBackwardConnectionsForUrlUseCase: GetBackwardConnectionsForUrlUseCase; // Connection use cases + getConnectionsUseCase: GetConnectionsUseCase; createConnectionUseCase: CreateConnectionUseCase; updateConnectionUseCase: UpdateConnectionUseCase; deleteConnectionUseCase: DeleteConnectionUseCase; @@ -402,6 +404,11 @@ ), // Connection use cases + getConnectionsUseCase: new GetConnectionsUseCase( + repositories.connectionQueryRepository, + repositories.cardQueryRepository, + services.identityResolutionService, + ), createConnectionUseCase: new CreateConnectionUseCase( repositories.connectionRepository, services.connectionPublisher, diff --git a/src/modules/cards/infrastructure/http/controllers/GetConnectionsController.ts b/src/modules/cards/infrastructure/http/controllers/GetConnectionsController.ts new file mode 100644 --- /dev/null +++ b/src/modules/cards/infrastructure/http/controllers/GetConnectionsController.ts @@ -0,0 +1,63 @@ +import { Controller } from '../../../../../shared/infrastructure/http/Controller'; +import { Response } from 'express'; +import { GetConnectionsUseCase } from '../../../application/useCases/queries/GetConnectionsUseCase'; +import { AuthenticatedRequest } from '../../../../../shared/infrastructure/http/middleware/AuthMiddleware'; +import { + ConnectionSortField, + SortOrder, +} from '../../../domain/IConnectionQueryRepository'; +import { ConnectionTypeEnum } from '../../../domain/value-objects/ConnectionType'; + +export class GetConnectionsController extends Controller { + constructor(private getConnectionsUseCase: GetConnectionsUseCase) { + super(); + } + + async executeImpl(req: AuthenticatedRequest, res: Response): Promise { + try { + const { identifier } = req.params; + const callingUserId = req.did; + + if (!identifier) { + return this.badRequest(res, 'User identifier is required'); + } + + // Parse pagination parameters + const page = req.query.page ? parseInt(req.query.page as string, 10) : 1; + const limit = req.query.limit + ? parseInt(req.query.limit as string, 10) + : 20; + + // Parse sorting parameters + const sortBy = + (req.query.sortBy as ConnectionSortField) || + ConnectionSortField.CREATED_AT; + const sortOrder = (req.query.sortOrder as SortOrder) || SortOrder.DESC; + + // Parse connection types filter + let connectionTypes: ConnectionTypeEnum[] | undefined; + if (req.query.connectionTypes) { + const typesParam = req.query.connectionTypes as string; + connectionTypes = typesParam.split(',') as ConnectionTypeEnum[]; + } + + const result = await this.getConnectionsUseCase.execute({ + userId: identifier, + callingUserId, + page, + limit, + sortBy, + sortOrder, + connectionTypes, + }); + + if (result.isErr()) { + return this.fail(res, result.error); + } + + return this.ok(res, result.value); + } catch (error: any) { + return this.handleError(res, error); + } + } +} diff --git a/src/modules/cards/infrastructure/http/routes/connectionRoutes.ts b/src/modules/cards/infrastructure/http/routes/connectionRoutes.ts --- a/src/modules/cards/infrastructure/http/routes/connectionRoutes.ts +++ b/src/modules/cards/infrastructure/http/routes/connectionRoutes.ts @@ -2,6 +2,7 @@ import { CreateConnectionController } from '../controllers/CreateConnectionController'; import { UpdateConnectionController } from '../controllers/UpdateConnectionController'; import { DeleteConnectionController } from '../controllers/DeleteConnectionController'; +import { GetConnectionsController } from '../controllers/GetConnectionsController'; import { GetForwardConnectionsForUrlController } from '../controllers/GetForwardConnectionsForUrlController'; import { GetBackwardConnectionsForUrlController } from '../controllers/GetBackwardConnectionsForUrlController'; import { AuthMiddleware } from 'src/shared/infrastructure/http/middleware'; @@ -11,12 +12,18 @@ createConnectionController: CreateConnectionController, updateConnectionController: UpdateConnectionController, deleteConnectionController: DeleteConnectionController, + getConnectionsController: GetConnectionsController, getForwardConnectionsForUrlController: GetForwardConnectionsForUrlController, getBackwardConnectionsForUrlController: GetBackwardConnectionsForUrlController, ): Router { const router = Router(); // Query routes + // GET /api/connections/user/:identifier - Get all connections for a user (curator) + router.get('/user/:identifier', authMiddleware.optionalAuth(), (req, res) => + getConnectionsController.execute(req, res), + ); + // GET /api/connections/forward - Get forward connections for URL router.get('/forward', authMiddleware.optionalAuth(), (req, res) => getForwardConnectionsForUrlController.execute(req, res),