tangled
alpha
login
or
join now
cosmik.network
/
semble
43
fork
atom
A social knowledge tool for researchers built on ATProto
43
fork
atom
overview
issues
13
pulls
pipelines
formatting and linting
Wesley Finck
7 months ago
662a575f
cb10363c
+86
-22
4 changed files
expand all
collapse all
unified
split
docs
features
CollectionTextSearch.md
GUIDE.md
src
modules
cards
infrastructure
repositories
DrizzleCollectionQueryRepository.ts
tests
utils
InMemoryCollectionQueryRepository.ts
+23
-4
docs/features/CollectionTextSearch.md
···
1
1
# Collection Text Search Feature
2
2
3
3
## Overview
4
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
12
+
11
13
**Pattern**: Enhance `GetMyCollectionsUseCase` with search capability
12
14
13
15
**Pros**:
14
14
-
- Follows single responsibility - still "getting collections"
16
16
+
17
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
23
+
20
24
```typescript
21
25
// Update existing query interface
22
26
export interface GetMyCollectionsQuery {
···
38
42
```
39
43
40
44
**Changes Required**:
45
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
53
+
48
54
**Pattern**: Create separate `SearchMyCollectionsUseCase`
49
55
50
56
**Pros**:
57
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
64
+
57
65
```typescript
58
66
// New dedicated use case
59
67
export interface SearchMyCollectionsQuery {
···
75
83
```
76
84
77
85
**Changes Required**:
86
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
94
+
85
95
**Pattern**: Extend existing use case but add dedicated search endpoint
86
96
87
97
**Implementation**:
98
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
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
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
128
+
115
129
```typescript
116
130
// Update GetMyCollectionsQuery
117
131
export interface GetMyCollectionsQuery {
···
125
139
```
126
140
127
141
#### 3. Infrastructure Layer Updates
142
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
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
166
+
150
167
```sql
151
168
-- Example SQL for text search across name and description
152
152
-
SELECT * FROM collections
153
153
-
WHERE curator_id = ?
169
169
+
SELECT * FROM collections
170
170
+
WHERE curator_id = ?
154
171
AND (
155
155
-
name ILIKE '%search_term%'
172
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
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
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
+
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
19
+
18
20
- **Purpose**: Modify system state (Create, Update, Delete operations)
19
21
- **Example**: `AddUrlToLibraryUseCase`
20
20
-
- **Characteristics**:
22
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
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
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
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
55
+
50
56
- **Location**: `src/modules/{module}/domain/`
51
51
-
- **Files**:
57
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
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
72
+
65
73
```typescript
66
74
// Example: AddUrlToLibraryUseCase
67
75
export interface AddUrlToLibraryDTO {
···
71
79
curatorId: string;
72
80
}
73
81
74
74
-
export class AddUrlToLibraryUseCase implements UseCase<AddUrlToLibraryDTO, Result<ResponseDTO>> {
82
82
+
export class AddUrlToLibraryUseCase
83
83
+
implements UseCase<AddUrlToLibraryDTO, Result<ResponseDTO>>
84
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
103
+
93
104
```typescript
94
105
// Example: GetCollectionPageUseCase
95
106
export interface GetCollectionPageQuery {
···
100
111
sortOrder?: SortOrder;
101
112
}
102
113
103
103
-
export class GetCollectionPageUseCase implements UseCase<GetCollectionPageQuery, Result<GetCollectionPageResult>> {
114
114
+
export class GetCollectionPageUseCase
115
115
+
implements UseCase<GetCollectionPageQuery, Result<GetCollectionPageResult>>
116
116
+
{
104
117
constructor(
105
118
private collectionRepo: ICollectionRepository,
106
119
private cardQueryRepo: ICardQueryRepository,
107
120
private profileService: IProfileService,
108
121
) {}
109
122
110
110
-
async execute(query: GetCollectionPageQuery): Promise<Result<GetCollectionPageResult>> {
123
123
+
async execute(
124
124
+
query: GetCollectionPageQuery,
125
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
137
+
122
138
- **Location**: `src/modules/{module}/infrastructure/repositories/`
123
123
-
- **Files**:
139
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
146
+
130
147
- **Location**: `src/modules/{module}/infrastructure/http/controllers/`
131
148
- **Pattern**:
149
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
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
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
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
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
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
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
237
+
213
238
- [ ] Implement command repository (if new entity)
214
214
-
- [ ] Implement query repository (if new entity)
239
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
245
+
220
246
- [ ] Define request/response types
221
247
- [ ] Implement client methods
222
248
- [ ] Update main ApiClient facade
223
249
224
250
### Integration
251
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
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
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
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
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
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
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
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
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
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
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
-
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
39
-
ilike(collections.description, searchTerm)
40
40
-
)!
39
39
+
ilike(collections.description, searchTerm),
40
40
+
)!,
41
41
);
42
42
}
43
43
···
53
53
cardCount: collections.cardCount,
54
54
})
55
55
.from(collections)
56
56
-
.where(sql`${whereConditions.reduce((acc, condition, index) =>
57
57
-
index === 0 ? condition : sql`${acc} AND ${condition}`
58
58
-
)}`)
56
56
+
.where(
57
57
+
sql`${whereConditions.reduce((acc, condition, index) =>
58
58
+
index === 0 ? condition : sql`${acc} AND ${condition}`,
59
59
+
)}`,
60
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
69
-
.where(sql`${whereConditions.reduce((acc, condition, index) =>
70
70
-
index === 0 ? condition : sql`${acc} AND ${condition}`
71
71
-
)}`);
71
71
+
.where(
72
72
+
sql`${whereConditions.reduce((acc, condition, index) =>
73
73
+
index === 0 ? condition : sql`${acc} AND ${condition}`,
74
74
+
)}`,
75
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
33
-
const nameMatch = collection.name.value.toLowerCase().includes(searchTerm);
34
34
-
const descriptionMatch = collection.description?.value.toLowerCase().includes(searchTerm) || false;
33
33
+
const nameMatch = collection.name.value
34
34
+
.toLowerCase()
35
35
+
.includes(searchTerm);
36
36
+
const descriptionMatch =
37
37
+
collection.description?.value.toLowerCase().includes(searchTerm) ||
38
38
+
false;
35
39
return nameMatch || descriptionMatch;
36
40
});
37
41
}