A social knowledge tool for researchers built on ATProto

formatting and linting

+86 -22
+23 -4
docs/features/CollectionTextSearch.md
··· 1 1 # Collection Text Search Feature 2 2 3 3 ## Overview 4 + 4 5 Enable text search over a user's collections so they can quickly find and select existing collections when adding cards. 5 6 6 7 ## Implementation Options ··· 8 9 Based on our CQRS and DDD architecture patterns, we have several strategic approaches: 9 10 10 11 ### Option 1: Extend Existing Query Use Case (Recommended) 12 + 11 13 **Pattern**: Enhance `GetMyCollectionsUseCase` with search capability 12 14 13 15 **Pros**: 14 - - Follows single responsibility - still "getting collections" 16 + 17 + - Follows single responsibility - still "getting collections" 15 18 - Reuses existing pagination, sorting, and enrichment logic 16 19 - Maintains consistent API surface 17 20 - Simpler client-side implementation 18 21 19 22 **Implementation**: 23 + 20 24 ```typescript 21 25 // Update existing query interface 22 26 export interface GetMyCollectionsQuery { ··· 38 42 ``` 39 43 40 44 **Changes Required**: 45 + 41 46 - Update `GetMyCollectionsQuery` interface 42 47 - Update `ICollectionQueryRepository.findByCreator()` method signature 43 48 - Update `DrizzleCollectionQueryRepository` implementation with text search logic ··· 45 50 - Update HTTP controller to handle search parameter 46 51 47 52 ### Option 2: Dedicated Search Use Case 53 + 48 54 **Pattern**: Create separate `SearchMyCollectionsUseCase` 49 55 50 56 **Pros**: 57 + 51 58 - Clear separation of concerns 52 59 - Optimized specifically for search scenarios 53 60 - Can implement different search algorithms/ranking 54 61 - Easier to add search-specific features (highlighting, relevance scoring) 55 62 56 63 **Implementation**: 64 + 57 65 ```typescript 58 66 // New dedicated use case 59 67 export interface SearchMyCollectionsQuery { ··· 75 83 ``` 76 84 77 85 **Changes Required**: 86 + 78 87 - Create new `SearchMyCollectionsUseCase` 79 88 - Create new repository method or separate search repository 80 89 - Create new HTTP controller and route ··· 82 91 - Implement search-specific DTOs 83 92 84 93 ### Option 3: Hybrid Approach 94 + 85 95 **Pattern**: Extend existing use case but add dedicated search endpoint 86 96 87 97 **Implementation**: 98 + 88 99 - Keep enhanced `GetMyCollectionsUseCase` for general listing with optional search 89 100 - Add dedicated `SearchMyCollectionsUseCase` for advanced search features 90 101 - Both share the same underlying repository search capability ··· 92 103 ## Recommended Approach: Option 1 (Extended Query) 93 104 94 105 ### Rationale 106 + 95 107 1. **Consistency**: Aligns with existing patterns where query use cases handle filtering/searching 96 108 2. **Simplicity**: Single endpoint for both listing and searching collections 97 109 3. **Client Efficiency**: No need to switch between different API methods ··· 100 112 ### Implementation Plan 101 113 102 114 #### 1. Domain Layer Updates 115 + 103 116 ```typescript 104 117 // Update ICollectionQueryRepository interface 105 118 export interface CollectionQueryOptions { ··· 112 125 ``` 113 126 114 127 #### 2. Application Layer Updates 128 + 115 129 ```typescript 116 130 // Update GetMyCollectionsQuery 117 131 export interface GetMyCollectionsQuery { ··· 125 139 ``` 126 140 127 141 #### 3. Infrastructure Layer Updates 142 + 128 143 - Update `DrizzleCollectionQueryRepository.findByCreator()` to handle text search 129 144 - Implement SQL text search (LIKE, full-text search, or similar) 130 145 - Update HTTP controller to accept search parameter 131 146 - Update route parameter handling 132 147 133 148 #### 4. API Client Updates 149 + 134 150 ```typescript 135 151 // Update existing types 136 152 export interface GetMyCollectionsParams { ··· 147 163 ### Search Implementation Details 148 164 149 165 #### Database Search Strategy 166 + 150 167 ```sql 151 168 -- Example SQL for text search across name and description 152 - SELECT * FROM collections 153 - WHERE curator_id = ? 169 + SELECT * FROM collections 170 + WHERE curator_id = ? 154 171 AND ( 155 - name ILIKE '%search_term%' 172 + name ILIKE '%search_term%' 156 173 OR description ILIKE '%search_term%' 157 174 ) 158 175 ORDER BY name ASC ··· 160 177 ``` 161 178 162 179 #### Search Behavior 180 + 163 181 - **Empty/null search**: Return all collections (existing behavior) 164 182 - **Search scope**: Collection name and description fields 165 183 - **Search type**: Case-insensitive partial matching (can be enhanced later) 166 184 - **Sorting**: Maintain existing sort options, could add relevance sorting later 167 185 168 186 ### Migration Path 187 + 169 188 1. Update domain interfaces (backward compatible) 170 189 2. Update use case (backward compatible - search is optional) 171 190 3. Update repository implementation
+44 -7
docs/features/GUIDE.md
··· 5 5 ## Architecture Overview 6 6 7 7 Our system follows a clean architecture pattern with the following layers: 8 + 8 9 - **Domain Layer**: Core business logic, entities, value objects, and domain services 9 10 - **Application Layer**: Use cases (commands and queries), DTOs, and application services 10 11 - **Infrastructure Layer**: Repositories, external services, and technical implementations ··· 15 16 We implement CQRS to separate read and write operations: 16 17 17 18 ### Commands 19 + 18 20 - **Purpose**: Modify system state (Create, Update, Delete operations) 19 21 - **Example**: `AddUrlToLibraryUseCase` 20 - - **Characteristics**: 22 + - **Characteristics**: 21 23 - Return success/failure results 22 24 - May trigger domain events 23 25 - Often involve business rule validation 24 26 - Use domain services for complex operations 25 27 26 28 ### Queries 29 + 27 30 - **Purpose**: Read data without side effects 28 31 - **Example**: `GetCollectionPageUseCase` 29 32 - **Characteristics**: ··· 37 40 ### 1. Domain Layer (if needed) 38 41 39 42 #### Domain Entities & Value Objects 43 + 40 44 - **Location**: `src/modules/{module}/domain/` 41 45 - **Files**: Entities, Value Objects, Domain Services 42 46 - **Example**: `Collection.ts`, `CardId.ts`, `URL.ts` 43 47 44 48 #### Domain Services 49 + 45 50 - **Location**: `src/modules/{module}/domain/services/` 46 51 - **Purpose**: Complex business logic that doesn't belong to a single entity 47 52 - **Example**: `CardLibraryService`, `CardCollectionService` 48 53 49 54 #### Repository Interfaces 55 + 50 56 - **Location**: `src/modules/{module}/domain/` 51 - - **Files**: 57 + - **Files**: 52 58 - `I{Entity}Repository.ts` - For command operations (write/modify state) 53 59 - `I{Entity}QueryRepository.ts` - For query operations (read-only, optimized for specific views) 54 60 - **Example**: `ICardRepository.ts`, `ICardQueryRepository.ts`, `ICollectionRepository.ts`, `ICollectionQueryRepository.ts` ··· 56 62 ### 2. Application Layer 57 63 58 64 #### Use Cases 65 + 59 66 - **Location**: `src/modules/{module}/application/useCases/` 60 67 - **Structure**: 61 68 - `commands/` - For state-changing operations 62 69 - `queries/` - For read-only operations 63 70 64 71 #### Command Use Case Pattern 72 + 65 73 ```typescript 66 74 // Example: AddUrlToLibraryUseCase 67 75 export interface AddUrlToLibraryDTO { ··· 71 79 curatorId: string; 72 80 } 73 81 74 - export class AddUrlToLibraryUseCase implements UseCase<AddUrlToLibraryDTO, Result<ResponseDTO>> { 82 + export class AddUrlToLibraryUseCase 83 + implements UseCase<AddUrlToLibraryDTO, Result<ResponseDTO>> 84 + { 75 85 constructor( 76 86 private cardRepository: ICardRepository, 77 87 private metadataService: IMetadataService, ··· 90 100 ``` 91 101 92 102 #### Query Use Case Pattern 103 + 93 104 ```typescript 94 105 // Example: GetCollectionPageUseCase 95 106 export interface GetCollectionPageQuery { ··· 100 111 sortOrder?: SortOrder; 101 112 } 102 113 103 - export class GetCollectionPageUseCase implements UseCase<GetCollectionPageQuery, Result<GetCollectionPageResult>> { 114 + export class GetCollectionPageUseCase 115 + implements UseCase<GetCollectionPageQuery, Result<GetCollectionPageResult>> 116 + { 104 117 constructor( 105 118 private collectionRepo: ICollectionRepository, 106 119 private cardQueryRepo: ICardQueryRepository, 107 120 private profileService: IProfileService, 108 121 ) {} 109 122 110 - async execute(query: GetCollectionPageQuery): Promise<Result<GetCollectionPageResult>> { 123 + async execute( 124 + query: GetCollectionPageQuery, 125 + ): Promise<Result<GetCollectionPageResult>> { 111 126 // 1. Validate query parameters 112 127 // 2. Fetch data from query repositories 113 128 // 3. Aggregate and transform data ··· 119 134 ### 3. Infrastructure Layer 120 135 121 136 #### Repository Implementations 137 + 122 138 - **Location**: `src/modules/{module}/infrastructure/repositories/` 123 - - **Files**: 139 + - **Files**: 124 140 - `Drizzle{Entity}Repository.ts` - Implements command repository interface 125 141 - `Drizzle{Entity}QueryRepository.ts` - Implements query repository interface with optimized read operations 126 142 - **Purpose**: Implement domain repository interfaces with specific technology (Drizzle ORM) 127 143 - **Pattern**: Query repositories often return DTOs optimized for specific views, while command repositories work with full domain entities 128 144 129 145 #### HTTP Controllers 146 + 130 147 - **Location**: `src/modules/{module}/infrastructure/http/controllers/` 131 148 - **Pattern**: 149 + 132 150 ```typescript 133 151 export class {Feature}Controller extends Controller { 134 152 constructor(private {feature}UseCase: {Feature}UseCase) { ··· 148 166 ``` 149 167 150 168 #### Routes 169 + 151 170 - **Location**: `src/modules/{module}/infrastructure/http/routes/` 152 171 - **Purpose**: Define HTTP endpoints and wire controllers 153 172 - **Pattern**: Group related endpoints, apply middleware (auth, validation) ··· 155 174 ### 4. Dependency Injection & Factories 156 175 157 176 #### Factory Registration 177 + 158 178 All new components must be registered in the appropriate factories: 159 179 160 180 1. **RepositoryFactory** (`src/shared/infrastructure/http/factories/RepositoryFactory.ts`) ··· 172 192 ### 5. API Client Layer 173 193 174 194 #### Client Structure 195 + 175 196 - **Location**: `src/webapp/api-client/` 176 197 - **Files**: 177 198 - `types/requests.ts` - Request DTOs ··· 180 201 - `ApiClient.ts` - Main client facade 181 202 182 203 #### Client Pattern 204 + 183 205 ```typescript 184 206 export class {Module}Client extends BaseClient { 185 207 async {operation}(request: {Operation}Request): Promise<{Operation}Response> { ··· 197 219 When implementing a new feature, follow this checklist: 198 220 199 221 ### Domain Layer 222 + 200 223 - [ ] Create/update domain entities if needed 201 224 - [ ] Create/update value objects if needed 202 225 - [ ] Define command repository interfaces (for write operations) ··· 204 227 - [ ] Implement domain services for complex business logic 205 228 206 229 ### Application Layer 230 + 207 231 - [ ] Create use case (command or query) 208 232 - [ ] Define request/response DTOs 209 233 - [ ] Implement business logic and validation 210 234 - [ ] Handle error cases appropriately 211 235 212 236 ### Infrastructure Layer 237 + 213 238 - [ ] Implement command repository (if new entity) 214 - - [ ] Implement query repository (if new entity) 239 + - [ ] Implement query repository (if new entity) 215 240 - [ ] Create HTTP controller 216 241 - [ ] Define routes 217 242 - [ ] Register in factories 218 243 219 244 ### API Client Layer 245 + 220 246 - [ ] Define request/response types 221 247 - [ ] Implement client methods 222 248 - [ ] Update main ApiClient facade 223 249 224 250 ### Integration 251 + 225 252 - [ ] Register all components in factories 226 253 - [ ] Wire routes in main app 227 254 - [ ] Test end-to-end flow 228 255 229 256 ### Repository Pattern (CQRS) 257 + 230 258 - **Command Repositories**: Handle write operations, work with full domain entities, enforce business rules 231 259 - **Query Repositories**: Handle read operations, return optimized DTOs, support pagination and sorting 232 260 - **Separation**: Commands use `I{Entity}Repository`, queries use `I{Entity}QueryRepository` ··· 235 263 ## Key Patterns & Conventions 236 264 237 265 ### Error Handling 266 + 238 267 - Use `Result<T, E>` pattern for use cases 239 268 - Define specific error types that extend `UseCaseError` 240 269 - Controllers handle errors and return appropriate HTTP status codes 241 270 242 271 ### Validation 272 + 243 273 - Input validation in use cases using value objects 244 274 - Domain validation in entities and value objects 245 275 - HTTP validation in controllers 246 276 247 277 ### Authentication 278 + 248 279 - Use `AuthenticatedRequest` for protected endpoints 249 280 - Extract user identity (`did`) from request 250 281 - Pass user context to use cases 251 282 252 283 ### Pagination & Sorting 284 + 253 285 - Standardize pagination parameters (`page`, `limit`) 254 286 - Use enums for sort fields and order 255 287 - Return pagination metadata in responses 256 288 257 289 ### Testing Strategy 290 + 258 291 - Unit tests for domain logic 259 292 - Integration tests for use cases 260 293 - In-memory implementations for testing ··· 271 304 ## Example Files to Reference 272 305 273 306 ### Command Example 307 + 274 308 - Use Case: `src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts` 275 309 - Controller: `src/modules/cards/infrastructure/http/controllers/AddUrlToLibraryController.ts` 276 310 277 311 ### Query Example 312 + 278 313 - Use Case: `src/modules/cards/application/useCases/queries/GetCollectionPageUseCase.ts` 279 314 - Controller: `src/modules/cards/infrastructure/http/controllers/GetCollectionPageController.ts` 280 315 281 316 ### Repository Examples 317 + 282 318 - Command Repository Interface: `src/modules/cards/domain/ICardRepository.ts` 283 319 - Query Repository Interface: `src/modules/cards/domain/ICardQueryRepository.ts` 284 320 - Command Repository Implementation: `src/modules/cards/infrastructure/repositories/DrizzleCardRepository.ts` 285 321 - Query Repository Implementation: `src/modules/cards/infrastructure/repositories/DrizzleCardQueryRepository.ts` 286 322 287 323 ### Factory Examples 324 + 288 325 - All factories in `src/shared/infrastructure/http/factories/` 289 326 290 327 This guide should be used as a reference when implementing new features to ensure consistency with the established architecture and patterns.
+13 -9
src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts
··· 29 29 30 30 // Build where conditions 31 31 const whereConditions = [eq(collections.authorId, curatorId)]; 32 - 32 + 33 33 // Add search condition if searchText is provided 34 34 if (searchText && searchText.trim()) { 35 35 const searchTerm = `%${searchText.trim()}%`; 36 36 whereConditions.push( 37 37 or( 38 38 ilike(collections.name, searchTerm), 39 - ilike(collections.description, searchTerm) 40 - )! 39 + ilike(collections.description, searchTerm), 40 + )!, 41 41 ); 42 42 } 43 43 ··· 53 53 cardCount: collections.cardCount, 54 54 }) 55 55 .from(collections) 56 - .where(sql`${whereConditions.reduce((acc, condition, index) => 57 - index === 0 ? condition : sql`${acc} AND ${condition}` 58 - )}`) 56 + .where( 57 + sql`${whereConditions.reduce((acc, condition, index) => 58 + index === 0 ? condition : sql`${acc} AND ${condition}`, 59 + )}`, 60 + ) 59 61 .orderBy(orderDirection(this.getSortColumn(sortBy))) 60 62 .limit(limit) 61 63 .offset(offset); ··· 66 68 const totalCountResult = await this.db 67 69 .select({ count: count() }) 68 70 .from(collections) 69 - .where(sql`${whereConditions.reduce((acc, condition, index) => 70 - index === 0 ? condition : sql`${acc} AND ${condition}` 71 - )}`); 71 + .where( 72 + sql`${whereConditions.reduce((acc, condition, index) => 73 + index === 0 ? condition : sql`${acc} AND ${condition}`, 74 + )}`, 75 + ); 72 76 73 77 const totalCount = totalCountResult[0]?.count || 0; 74 78 const hasMore = offset + collectionsResult.length < totalCount;
+6 -2
src/modules/cards/tests/utils/InMemoryCollectionQueryRepository.ts
··· 30 30 if (options.searchText && options.searchText.trim()) { 31 31 const searchTerm = options.searchText.trim().toLowerCase(); 32 32 creatorCollections = creatorCollections.filter((collection) => { 33 - const nameMatch = collection.name.value.toLowerCase().includes(searchTerm); 34 - const descriptionMatch = collection.description?.value.toLowerCase().includes(searchTerm) || false; 33 + const nameMatch = collection.name.value 34 + .toLowerCase() 35 + .includes(searchTerm); 36 + const descriptionMatch = 37 + collection.description?.value.toLowerCase().includes(searchTerm) || 38 + false; 35 39 return nameMatch || descriptionMatch; 36 40 }); 37 41 }