Skip to content

Commit 96e5945

Browse files
Merge pull request #15 from mauriciozanettisalomao/feat/lfxv2-199-org-search-endpoint
[LFX-V2-199] Query Service: org search endpoint implementation
2 parents 608d3b8 + e7cecee commit 96e5945

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3632
-231
lines changed

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ make lint
4141
# Or: golangci-lint run ./...
4242

4343
# Run specific test
44-
go test -v -run TestResourceSearch ./internal/usecase/
44+
go test -v -run TestResourceSearch ./internal/service/
4545
```
4646

4747
### Docker Operations
@@ -64,7 +64,7 @@ This service follows clean architecture principles with clear separation of conc
6464
- `model/`: Core business entities (Resource, SearchCriteria, AccessCheck)
6565
- `port/`: Interfaces defining contracts (ResourceSearcher, AccessControlChecker)
6666

67-
2. **Use Case Layer** (`internal/usecase/`)
67+
2. **Service Layer** (`internal/service/`)
6868
- Business logic orchestration
6969
- Coordinates between domain and infrastructure
7070

@@ -93,7 +93,7 @@ This service follows clean architecture principles with clear separation of conc
9393

9494
1. HTTP request → Goa generated server (`gen/http/query_svc/server/`)
9595
2. Service layer (`cmd/query_svc/query_svc.go`)
96-
3. Use case orchestration (`internal/usecase/resource_search.go`)
96+
3. Use case orchestration (`internal/service/resource_search.go`)
9797
4. Domain interfaces called with concrete implementations
9898
5. Response formatted and returned through Goa
9999

README.md

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The implementation follows the clean architecture principles where:
2525
│ ├── domain/ # Domain logic layer
2626
│ │ ├── model/ # Domain models and entities
2727
│ │ └── port/ # Domain interfaces/ports
28-
│ ├── usecase/ # Business logic/use cases layer
28+
│ ├── service/ # Business logic/use cases layer
2929
│ ├── infrastructure/ # Infrastructure layer
3030
│ └── middleware/ # HTTP middleware components
3131
└── pkg/ # Shared packages for internal and external services
@@ -47,10 +47,10 @@ The implementation follows the clean architecture principles where:
4747
- **ResourceSearcher Interface**: Defines the contract for resource search operations
4848
- **AccessControlChecker Interface**: Defines the contract for access control operations
4949

50-
### Use Case Layer (`internal/usecase/`)
50+
### Service Layer (`internal/service/`)
5151

5252
- **Business Logic**: Application-specific business rules and operations
53-
- **Use Case Orchestration**: Coordinates between domain models and infrastructure
53+
- **Service Orchestration**: Coordinates between domain models and infrastructure
5454

5555
### Infrastructure Layer (`internal/infrastructure/`)
5656

@@ -62,6 +62,10 @@ The OpenSearch implementation includes query templates, a searcher, and a client
6262

6363
The NATS implementation consists of a client, access control logic, and request/response models for messaging and access control.
6464

65+
#### Clearbit Implementation
66+
67+
The Clearbit implementation provides organization search capabilities using the Clearbit Company API. It includes a client for API communication, searcher for organization queries, and configuration management for API credentials and settings.
68+
6569
## Dependency Injection
6670

6771
Dependency injection is performed in `cmd/main.go`, where the concrete implementations for resource search and access control are selected based on configuration and then injected into the service constructor.
@@ -106,15 +110,21 @@ SEARCH_SOURCE=mock ACCESS_CONTROL_SOURCE=mock go run cmd/main.go
106110
SEARCH_SOURCE=mock ACCESS_CONTROL_SOURCE=mock go run cmd/main.go -p 3000
107111
```
108112

109-
#### With OpenSearch and NATS
113+
#### With Production Services
110114

111115
```bash
112-
# Using OpenSearch and NATS (production-like setup)
116+
# production-like setup
113117
SEARCH_SOURCE=opensearch \
118+
ORG_SEARCH_SOURCE=clearbit \
114119
ACCESS_CONTROL_SOURCE=nats \
115120
OPENSEARCH_URL={{placeholder}} \
116121
OPENSEARCH_INDEX=resources \
117122
NATS_URL{{placeholder}} \
123+
CLEARBIT_CREDENTIAL=your_clearbit_api_key \
124+
CLEARBIT_BASE_URL=https://company.clearbit.com \
125+
CLEARBIT_TIMEOUT=30s \
126+
CLEARBIT_MAX_RETRIES=5 \
127+
CLEARBIT_RETRY_DELAY=2s \
118128
go run cmd/main.go
119129
```
120130

@@ -124,6 +134,10 @@ go run cmd/main.go
124134

125135
- `SEARCH_SOURCE`: Choose between "mock" or "opensearch" (default: "opensearch")
126136

137+
**Organization Search Implementation:**
138+
139+
- `ORG_SEARCH_SOURCE`: Choose between "mock" or "clearbit" (default: "clearbit")
140+
127141
**OpenSearch Configuration:**
128142

129143
- `OPENSEARCH_URL`: OpenSearch URL (default: `http://localhost:9200`)
@@ -140,6 +154,14 @@ go run cmd/main.go
140154
- `NATS_MAX_RECONNECT`: Maximum reconnection attempts (default: "3")
141155
- `NATS_RECONNECT_WAIT`: Time between reconnection attempts (default: "2s")
142156

157+
**Clearbit Configuration:**
158+
159+
- `CLEARBIT_CREDENTIAL`: Clearbit API key (required for organization search)
160+
- `CLEARBIT_BASE_URL`: Clearbit API base URL (default: `https://company.clearbit.com`)
161+
- `CLEARBIT_TIMEOUT`: HTTP client timeout for API requests (default: "10s")
162+
- `CLEARBIT_MAX_RETRIES`: Maximum number of retry attempts for failed requests (default: "3")
163+
- `CLEARBIT_RETRY_DELAY`: Delay between retry attempts (default: "1s")
164+
143165
**Server Configuration:**
144166

145167
- `-p`: HTTP port (default: "8080")
@@ -184,6 +206,59 @@ GET /query/resources?name=committee&type=committee&v=1
184206
}
185207
```
186208

209+
## Clearbit API Integration
210+
211+
The service integrates with Clearbit's Company API to provide enriched organization data for search operations. This integration allows the service to fetch detailed company information including industry classification, employee count, and domain information.
212+
213+
### Clearbit API Setup
214+
215+
#### 1. Obtain API Credentials
216+
217+
To use Clearbit integration, you need to obtain an API key from Clearbit:
218+
219+
1. Sign up for a Clearbit account at [https://clearbit.com](https://clearbit.com)
220+
2. Navigate to your API settings to generate an API key
221+
3. Copy the API key for use in your environment configuration
222+
223+
#### 2. Configure Environment Variables
224+
225+
Set the required environment variables for Clearbit integration:
226+
227+
```bash
228+
# Required: Clearbit API key
229+
export CLEARBIT_CREDENTIAL=your_clearbit_api_key_here
230+
231+
# Optional: Custom configuration (defaults shown)
232+
export CLEARBIT_BASE_URL=https://company.clearbit.com
233+
export CLEARBIT_TIMEOUT=30s
234+
export CLEARBIT_MAX_RETRIES=3
235+
export CLEARBIT_RETRY_DELAY=1s
236+
237+
# Set organization search source to use Clearbit
238+
export ORG_SEARCH_SOURCE=clearbit
239+
```
240+
241+
#### 3. API Usage and Features
242+
243+
The Clearbit integration supports the following search operations:
244+
245+
**Search by Company Name:**
246+
- Searches for companies using their registered business name
247+
- Falls back to domain-based search for additional data enrichment
248+
249+
**Search by Domain:**
250+
- More accurate search method using company domain names
251+
- Provides comprehensive company information
252+
253+
#### 4. Error Handling
254+
255+
The Clearbit integration includes robust error handling:
256+
257+
- **404 Not Found**: Returns appropriate "organization not found" errors
258+
- **Rate Limiting**: Automatic retry with exponential backoff
259+
- **Network Issues**: Configurable retry attempts with delays
260+
- **API Errors**: Proper error propagation with context
261+
187262
### Testing
188263

189264
The clean architecture makes testing straightforward:

charts/lfx-v2-query-service/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ apiVersion: v2
55
name: lfx-v2-query-service
66
description: LFX Platform V2 Query Service chart
77
type: application
8-
version: 0.2.5
8+
version: 0.4.0
99
appVersion: "latest"

cmd/main.go

Lines changed: 6 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,15 @@ import (
77
"context"
88
"flag"
99
"fmt"
10-
"log"
1110
"log/slog"
1211
"os"
1312
"os/signal"
14-
"strconv"
1513
"sync"
1614
"syscall"
1715
"time"
1816

19-
querysvcapi "github.com/linuxfoundation/lfx-v2-query-service/cmd/query_svc"
17+
"github.com/linuxfoundation/lfx-v2-query-service/cmd/service"
2018
querysvc "github.com/linuxfoundation/lfx-v2-query-service/gen/query_svc"
21-
"github.com/linuxfoundation/lfx-v2-query-service/internal/domain/port"
22-
"github.com/linuxfoundation/lfx-v2-query-service/internal/infrastructure/mock"
23-
"github.com/linuxfoundation/lfx-v2-query-service/internal/infrastructure/nats"
24-
"github.com/linuxfoundation/lfx-v2-query-service/internal/infrastructure/opensearch"
2519
logging "github.com/linuxfoundation/lfx-v2-query-service/pkg/log"
2620
"goa.design/clue/debug"
2721
)
@@ -61,15 +55,16 @@ func main() {
6155
)
6256

6357
// Initialize the resource searcher based on configuration
64-
resourceSearcher := searcherImpl(ctx)
65-
accessControlChecker := accessControlCheckerImpl(ctx)
58+
resourceSearcher := service.SearcherImpl(ctx)
59+
accessControlChecker := service.AccessControlCheckerImpl(ctx)
60+
organizationSearcher := service.OrganizationSearcherImpl(ctx)
6661

6762
// Initialize the services.
6863
var (
6964
querySvcSvc querysvc.Service
7065
)
7166
{
72-
querySvcSvc = querysvcapi.NewQuerySvc(resourceSearcher, accessControlChecker)
67+
querySvcSvc = service.NewQuerySvc(resourceSearcher, accessControlChecker, organizationSearcher)
7368
}
7469

7570
// Wrap the services in endpoints that can be invoked from other services
@@ -93,7 +88,7 @@ func main() {
9388
ctx, cancel := context.WithCancel(ctx)
9489

9590
// Setup the JWT authentication which validates and parses the JWT token.
96-
querysvcapi.SetupJWTAuth(ctx)
91+
service.SetupJWTAuth(ctx)
9792

9893
// Start the servers and send errors (if any) to the error channel.
9994
addr := ":" + *port
@@ -141,128 +136,3 @@ func main() {
141136

142137
slog.InfoContext(ctx, "exited")
143138
}
144-
145-
func searcherImpl(ctx context.Context) port.ResourceSearcher {
146-
147-
var (
148-
resourceSearcher port.ResourceSearcher
149-
err error
150-
)
151-
152-
// Search source implementation configuration
153-
searchSource := os.Getenv("SEARCH_SOURCE")
154-
if searchSource == "" {
155-
searchSource = "opensearch"
156-
}
157-
158-
opensearchURL := os.Getenv("OPENSEARCH_URL")
159-
if opensearchURL == "" {
160-
opensearchURL = "http://localhost:9200"
161-
}
162-
163-
opensearchIndex := os.Getenv("OPENSEARCH_INDEX")
164-
if opensearchIndex == "" {
165-
opensearchIndex = "resources"
166-
}
167-
168-
switch searchSource {
169-
case "mock":
170-
slog.InfoContext(ctx, "initializing mock resource searcher")
171-
resourceSearcher = mock.NewMockResourceSearcher()
172-
173-
case "opensearch":
174-
slog.InfoContext(ctx, "initializing opensearch resource searcher",
175-
"url", opensearchURL,
176-
"index", opensearchIndex,
177-
)
178-
opensearchConfig := opensearch.Config{
179-
URL: opensearchURL,
180-
Index: opensearchIndex,
181-
}
182-
183-
resourceSearcher, err = opensearch.NewSearcher(ctx, opensearchConfig)
184-
if err != nil {
185-
log.Fatalf("failed to initialize OpenSearch searcher: %v", err)
186-
}
187-
188-
default:
189-
log.Fatalf("unsupported search implementation: %s", searchSource)
190-
}
191-
192-
return resourceSearcher
193-
194-
}
195-
196-
func accessControlCheckerImpl(ctx context.Context) port.AccessControlChecker {
197-
198-
var (
199-
accessControlChecker port.AccessControlChecker
200-
err error
201-
)
202-
203-
// Access control implementation configuration
204-
accessControlSource := os.Getenv("ACCESS_CONTROL_SOURCE")
205-
if accessControlSource == "" {
206-
accessControlSource = "nats"
207-
}
208-
209-
natsURL := os.Getenv("NATS_URL")
210-
if natsURL == "" {
211-
natsURL = "nats://localhost:4222"
212-
}
213-
214-
natsTimeout := os.Getenv("NATS_TIMEOUT")
215-
if natsTimeout == "" {
216-
natsTimeout = "10s"
217-
}
218-
natsTimeoutDuration, err := time.ParseDuration(natsTimeout)
219-
if err != nil {
220-
log.Fatalf("invalid NATS timeout duration: %v", err)
221-
}
222-
223-
natsMaxReconnect := os.Getenv("NATS_MAX_RECONNECT")
224-
if natsMaxReconnect == "" {
225-
natsMaxReconnect = "3"
226-
}
227-
natsMaxReconnectInt, err := strconv.Atoi(natsMaxReconnect)
228-
if err != nil {
229-
log.Fatalf("invalid NATS max reconnect value %s: %v", natsMaxReconnect, err)
230-
}
231-
232-
natsReconnectWait := os.Getenv("NATS_RECONNECT_WAIT")
233-
if natsReconnectWait == "" {
234-
natsReconnectWait = "2s"
235-
}
236-
natsReconnectWaitDuration, err := time.ParseDuration(natsReconnectWait)
237-
if err != nil {
238-
log.Fatalf("invalid NATS reconnect wait duration %s : %v", natsReconnectWait, err)
239-
}
240-
241-
//natsReconnectWait := flag.Duration("nats-reconnect-wait", 2*time.Second, "NATS reconnection wait time")
242-
243-
// Initialize the access control checker based on configuration
244-
switch accessControlSource {
245-
case "mock":
246-
slog.InfoContext(ctx, "initializing mock access control checker")
247-
accessControlChecker = mock.NewMockAccessControlChecker()
248-
249-
case "nats":
250-
slog.InfoContext(ctx, "initializing NATS access control checker")
251-
natsConfig := nats.Config{
252-
URL: natsURL,
253-
Timeout: natsTimeoutDuration,
254-
MaxReconnect: natsMaxReconnectInt,
255-
ReconnectWait: natsReconnectWaitDuration,
256-
}
257-
258-
accessControlChecker, err = nats.NewAccessControlChecker(ctx, natsConfig)
259-
if err != nil {
260-
log.Fatalf("failed to initialize NATS access control checker: %v", err)
261-
}
262-
263-
default:
264-
log.Fatalf("unsupported access control implementation: %s", accessControlSource)
265-
}
266-
267-
return accessControlChecker
268-
}

0 commit comments

Comments
 (0)