From ba4af30e295caec6f1447cc609084977d4bdc28f Mon Sep 17 00:00:00 2001 From: setiadha Date: Mon, 23 Jun 2025 08:29:12 -0400 Subject: [PATCH 1/7] direct db to api:init --- .../Api/DiseaseLookupController.php | 54 ++++++++++++++----- .../Controllers/Api/GeneLookupController.php | 7 +-- app/Services/GtApi/AccessTokenManager.php | 45 ++++++++++++++++ app/Services/GtApi/GtApiClient.php | 41 ++++++++++++++ app/Services/GtApi/GtApiService.php | 50 +++++++++++++++++ config/services.php | 6 +++ 6 files changed, 186 insertions(+), 17 deletions(-) create mode 100644 app/Services/GtApi/AccessTokenManager.php create mode 100644 app/Services/GtApi/GtApiClient.php create mode 100644 app/Services/GtApi/GtApiService.php diff --git a/app/Http/Controllers/Api/DiseaseLookupController.php b/app/Http/Controllers/Api/DiseaseLookupController.php index 9ac9f566e..2c13d7f39 100644 --- a/app/Http/Controllers/Api/DiseaseLookupController.php +++ b/app/Http/Controllers/Api/DiseaseLookupController.php @@ -8,8 +8,17 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; +use App\Services\GtApi\GtApiService; + class DiseaseLookupController extends Controller { + protected GtApiService $gtApi; + + public function __construct(GtApiService $gtApi) + { + $this->gtApi = $gtApi; + } + public function show($mondoId) { $validator = Validator::make(['mondo_id' => $mondoId], [ @@ -19,24 +28,41 @@ public function show($mondoId) throw new ValidationException($validator); } - return DB::connection(config('database.gt_db_connection'))->table('diseases')->where('mondo_id', $mondoId)->sole(); + $mondo_id = strtolower($validator->validated()['mondo_id']); + // return DB::connection(config('database.gt_db_connection'))->table('diseases')->where('mondo_id', $mondoId)->sole(); + try { + $result = $this->gtApi->getDiseaseByMondoId($mondo_id); + return response()->json($result); + } catch (\Exception $e) { + return response()->json([ + 'error' => 'Failed to retrieve disease data.', + 'details' => $e->getMessage(), + ], 500); + } } public function search(Request $request) - { - $queryString = strtolower(($request->query_string ?? '')); - if (strlen($queryString) < 3) { - return []; + { + + $validator = Validator::make($request->all(), [ + 'query_string' => ['required', 'string', 'min:3'], + ]); + + if ($validator->fails()) { + return $this->errorResponse('Validation failed', 422, $validator->errors()); } + + $query = strtolower($validator->validated()['query_string']); + - $results = DB::connection(config('database.gt_db_connection'))->table('diseases') - ->select('id', 'mondo_id', 'doid_id', 'name',) - ->where('name', 'like', '%'.$queryString.'%') - ->orWhere('mondo_id', 'like', '%'.$queryString.'%') - ->orWhere('doid_id', 'like', '%'.$queryString.'%') - ->limit(50) - ->get(); - - return $results->toArray(); + try { + $result = $this->gtApi->searchDiseases($query); + return response()->json($result); + } catch (\Exception $e) { + return response()->json([ + 'error' => 'Failed to search disease data.', + 'details' => $e->getMessage(), + ], 500); + } } } diff --git a/app/Http/Controllers/Api/GeneLookupController.php b/app/Http/Controllers/Api/GeneLookupController.php index 630d4571c..6bdf69604 100644 --- a/app/Http/Controllers/Api/GeneLookupController.php +++ b/app/Http/Controllers/Api/GeneLookupController.php @@ -5,16 +5,17 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use App\Http\Controllers\Controller; +use Illuminate\Support\Facades\Log; class GeneLookupController extends Controller { + protected GtApiService $gtApi; - public function show($hgncId) + public function __construct(GtApiService $gtApi) { - return DB::connection(config('database.gt_db_connection'))->table('hgnc_genes')->where('hgnc_id', $hgncId)->sole(); + $this->gtApi = $gtApi; } - public function search(Request $request) { $queryString = strtolower(($request->query_string ?? '')); diff --git a/app/Services/GtApi/AccessTokenManager.php b/app/Services/GtApi/AccessTokenManager.php new file mode 100644 index 000000000..4f9a2c5ec --- /dev/null +++ b/app/Services/GtApi/AccessTokenManager.php @@ -0,0 +1,45 @@ +clientId = config('services.gt_api.client_id'); + $this->clientSecret = config('services.gt_api.client_secret'); + $this->tokenUrl = config('services.gt_api.oauth_url'); + } + + public function getToken(): string + { + // Check cache first + return Cache::remember('gt_api_access_token', 150, function () { + $response = Http::asForm()->post($this->tokenUrl, [ + 'grant_type' => 'client_credentials', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'scope' => '', + ]); + + if ($response->failed()) { + throw new \Exception('Failed to retrieve access token from GT API: ' . $response->body()); + } + + $data = $response->json(); + // Cache for slightly less than expires_in (default 180s) + Cache::put('gt_api_access_token', $data['access_token'], now()->addSeconds($data['expires_in'] - 10)); + + return $data['access_token']; + }); + } +} diff --git a/app/Services/GtApi/GtApiClient.php b/app/Services/GtApi/GtApiClient.php new file mode 100644 index 000000000..11d71aba7 --- /dev/null +++ b/app/Services/GtApi/GtApiClient.php @@ -0,0 +1,41 @@ +baseUrl = config('services.gt_api.base_url'); + $this->tokenManager = $tokenManager; + } + + protected function request(): \Illuminate\Http\Client\PendingRequest + { + $accessToken = $this->tokenManager->getToken(); + + return Http::timeout(10) + ->retry(2, 200) + ->withToken($accessToken) + ->acceptJson(); + } + + public function post(string $endpoint, array $payload = []): Response + { + try { + return $this->request() + ->post($this->baseUrl . $endpoint, $payload) + ->throw(); + } catch (RequestException $e) { + report($e); + throw new \Exception('GT API request failed: ' . $e->getMessage()); + } + } +} diff --git a/app/Services/GtApi/GtApiService.php b/app/Services/GtApi/GtApiService.php new file mode 100644 index 000000000..21177aa3f --- /dev/null +++ b/app/Services/GtApi/GtApiService.php @@ -0,0 +1,50 @@ +client = $client; + } + + public function searchGenes(string $query): array + { + $response = $this->client->post('/genes/search', ['query' => $query]); + return $response->json('results') ?? []; + } + + public function getGeneSymbolById(int $hgncId): array + { + $response = $this->client->post('/genes/byid', ['hgnc_id' => $hgncId]); + return $response->json(); + } + + public function getGeneSymbolBySymbol(string $symbol): array + { + $response = $this->client->post('/genes/bysymbol', ['gene_symbol' => $symbol]); + return $response->json(); + } + + public function searchDiseases(string $query): array + { + $response = $this->client->post('/diseases/search', ['query' => $query]); + return $response->json('results') ?? []; + } + + public function getDiseaseByMondoId(string $mondoId): array + { + $response = $this->client->post('/diseases/mondo', ['mondo_id' => $mondoId]); + return $response->json(); + } + + public function getDiseaseByOntologyId(string $ontologyId): array + { + $response = $this->client->post('/diseases/ontology', ['ontology_id' => $ontologyId]); + return $response->json(); + } +} diff --git a/config/services.php b/config/services.php index 2a1d616c7..775d6682f 100644 --- a/config/services.php +++ b/config/services.php @@ -30,4 +30,10 @@ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], + 'gt_api' => [ + 'client_id' => env('GT_CLIENT_API_ID'), + 'client_secret' => env('GT_CLIENT_API_SECRET'), + 'oauth_url' => env('GT_CLIENT_BASE_URL') . '/oauth/token', + 'base_url' => env('GT_CLIENT_BASE_URL') . env('GT_CLIENT_API'), + ], ]; From 3900c4ecef796c0671bfe75e275ad59933fed6bd Mon Sep 17 00:00:00 2001 From: setiadha Date: Tue, 24 Jun 2025 05:10:28 -0400 Subject: [PATCH 2/7] check button, bulk curation look up --- .../Api/DiseaseLookupController.php | 15 +- .../Controllers/Api/GeneLookupController.php | 55 ++++++- app/Services/DiseaseLookup.php | 30 ++-- app/Services/GtApi/GtApiService.php | 8 +- app/Services/HgncLookup.php | 53 ++++--- .../components/expert_panels/GcepGeneList.vue | 147 +++++++++++++++++- routes/api.php | 1 + 7 files changed, 262 insertions(+), 47 deletions(-) diff --git a/app/Http/Controllers/Api/DiseaseLookupController.php b/app/Http/Controllers/Api/DiseaseLookupController.php index 2c13d7f39..36bc7daa3 100644 --- a/app/Http/Controllers/Api/DiseaseLookupController.php +++ b/app/Http/Controllers/Api/DiseaseLookupController.php @@ -29,10 +29,12 @@ public function show($mondoId) } $mondo_id = strtolower($validator->validated()['mondo_id']); - // return DB::connection(config('database.gt_db_connection'))->table('diseases')->where('mondo_id', $mondoId)->sole(); try { - $result = $this->gtApi->getDiseaseByMondoId($mondo_id); - return response()->json($result); + $response = $this->gtApi->getDiseaseByMondoId($mondo_id); + if (!($response['success'] ?? false) || empty($response['data'])) { + throw new \Exception("Disease with MONDO ID $mondoId not found."); + } + return $response['data']; } catch (\Exception $e) { return response()->json([ 'error' => 'Failed to retrieve disease data.', @@ -56,8 +58,11 @@ public function search(Request $request) try { - $result = $this->gtApi->searchDiseases($query); - return response()->json($result); + $response = $this->gtApi->searchDiseases($query); + if (!($response['success'] ?? false) || empty($response['data'])) { + throw new \Exception("Disease with MONDO ID $mondoId not found."); + } + return $response['data']; } catch (\Exception $e) { return response()->json([ 'error' => 'Failed to search disease data.', diff --git a/app/Http/Controllers/Api/GeneLookupController.php b/app/Http/Controllers/Api/GeneLookupController.php index 6bdf69604..a912402c3 100644 --- a/app/Http/Controllers/Api/GeneLookupController.php +++ b/app/Http/Controllers/Api/GeneLookupController.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\DB; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Log; +use App\Services\GtApi\GtApiService; class GeneLookupController extends Controller { @@ -23,14 +24,52 @@ public function search(Request $request) if (strlen($queryString) < 3) { return []; } - $results = DB::connection(config('database.gt_db_connection'))->table('genes') - ->where('gene_symbol', 'like', '%'.$queryString.'%') - ->orWhere('hgnc_id', 'like', '%'.$queryString.'%') - ->limit(250) - ->get(); + try { + $response = $this->gtApi->searchGenes($queryString); - return $results->toArray(); + if (!($response['success'] ?? false)) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to search genes.', + 'errors' => $response['message'] ?? 'Unknown error' + ], 500); + } + + return $response['data']['results'] ?? []; + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Internal server error', + 'errors' => $e->getMessage() + ], 500); + } + } + + public function check(Request $request) + { + $symbols = $request->input('gene_symbol'); + + if (!$symbols || !is_string($symbols)) { + return response()->json([ + 'success' => false, + 'message' => 'gene_symbol must be a non-empty comma-separated string.', + 'data' => [] + ], 422); + } + + try { + $result = $this->gtApi->lookupGenesBulk($symbols); + + return response()->json([ + 'success' => true, + 'message' => 'Gene status retrieved.', + 'data' => $result['data'] ?? [], + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error checking gene status: ' . $e->getMessage(), + ], 500); + } } - - } diff --git a/app/Services/DiseaseLookup.php b/app/Services/DiseaseLookup.php index df49dd624..c27a29766 100644 --- a/app/Services/DiseaseLookup.php +++ b/app/Services/DiseaseLookup.php @@ -3,12 +3,19 @@ use App\Services\DiseaseLookupInterface; use Exception; -use Illuminate\Support\Facades\DB; +use App\Services\GtApi\GtApiService; class DiseaseLookup implements DiseaseLookupInterface { const SUPPORTED_ONTOLOGIES = ['mondo', 'doid']; + protected GtApiService $gtApiService; + + public function __construct(GtApiService $gtApiService) + { + $this->gtApiService = $gtApiService; + } + public function findNameByOntologyId(string $ontologyId): string { $ontology = strtolower(explode(':', $ontologyId)[0]); @@ -16,16 +23,19 @@ public function findNameByOntologyId(string $ontologyId): string throw new Exception('Ontology '.$ontology.' is not supported'); } - $diseaseData = DB::connection(config('database.gt_db_connection')) - ->table('diseases') - ->select('name') - ->where($ontology.'_id', $ontologyId) - ->first(); + try { + $response = $this->gtApi->getDiseaseByOntologyId($ontologyId); - if (!$diseaseData) { - throw new Exception('We couldn\'t find a disease with '. $ontology .' ID '.$ontologyId.' in our records.'); - } + if (!($response['success'] ?? false) || empty($response['data']['name'])) { + throw new \Exception("We couldn't find a disease with {$ontology} ID {$ontologyId} in our records."); + } - return $diseaseData->name; + return $response['data']['name']; + } catch (\Exception $e) { + return response()->json([ + 'error' => 'Failed to retrieve disease data.', + 'details' => $e->getMessage(), + ], 500); + } } } diff --git a/app/Services/GtApi/GtApiService.php b/app/Services/GtApi/GtApiService.php index 21177aa3f..b00258271 100644 --- a/app/Services/GtApi/GtApiService.php +++ b/app/Services/GtApi/GtApiService.php @@ -1,6 +1,5 @@ client->post('/diseases/ontology', ['ontology_id' => $ontologyId]); return $response->json(); } + + public function lookupGenesBulk(string $genes): array + { + $response = $this->client->post('/genes/curations', ['gene_symbol' => $genes]); + return $response->json(); + } + } diff --git a/app/Services/HgncLookup.php b/app/Services/HgncLookup.php index c25c7a3cc..a9131ac45 100644 --- a/app/Services/HgncLookup.php +++ b/app/Services/HgncLookup.php @@ -2,34 +2,51 @@ namespace App\Services; use Exception; -use Illuminate\Support\Facades\DB; use App\Services\HgncLookupInterface; +use App\Services\GtApi\GtApiService; class HgncLookup implements HgncLookupInterface { - public function findSymbolById($hgncId): string + protected GtApiService $gtApiService; + + public function __construct(GtApiService $gtApiService) { - $geneData = DB::connection(config('database.gt_db_connection')) - ->table('genes') - ->select('gene_symbol') - ->where('hgnc_id', $hgncId) - ->first(); - if (!$geneData) { - throw new Exception('No gene with HGNC ID '.$hgncId.' in our records.', 404); + $this->gtApiService = $gtApiService; + } + + public function findSymbolById($hgncId): string + { + try { + $response = $this->gtApiService->getGeneSymbolById((int)$hgncId); + + if (!($response['success'] ?? false) || empty($response['data']['gene_symbol'])) { + throw new Exception('No gene with HGNC ID ' . $hgncId . ' in our records.', 404); + } + + return $response['data']['gene_symbol']; + } catch (\Exception $e) { + return response()->json([ + 'error' => 'Failed to retrieve gene data.', + 'details' => $e->getMessage(), + ], 500); } - return $geneData->gene_symbol; } public function findHgncIdBySymbol($geneSymbol): int { - $geneData = DB::connection(config('database.gt_db_connection')) - ->table('genes') - ->select('hgnc_id') - ->where('gene_symbol', $geneSymbol) - ->first(); - if (!$geneData) { - throw new Exception('No gene with gene symbol '.$geneSymbol.' in our records.', 404); + try { + $response = $this->gtApiService->getGeneSymbolBySymbol((string)$geneSymbol); + + if (!($response['success'] ?? false) || empty($response['data']['hgnc_id'])) { + throw new Exception('No gene with gene symbol ' . $geneSymbol . ' in our records.', 404); + } + + return (int)$response['data']['hgnc_id']; + } catch (\Exception $e) { + return response()->json([ + 'error' => 'Failed to retrieve gene data.', + 'details' => $e->getMessage(), + ], 500); } - return $geneData->hgnc_id; } } diff --git a/resources/js/components/expert_panels/GcepGeneList.vue b/resources/js/components/expert_panels/GcepGeneList.vue index c05419a9e..81860c9cb 100644 --- a/resources/js/components/expert_panels/GcepGeneList.vue +++ b/resources/js/components/expert_panels/GcepGeneList.vue @@ -57,7 +57,6 @@ export default { store.commit('pushError', error.response.data); } loading.value = false; - } const hideForm = () => { context.emit('update:editing', false); @@ -79,7 +78,7 @@ export default { : null }; const save = async () => { - const genes = genesAsText.value + const genes = genesAsText.value ? genesAsText.value .split(/[, \n]/) .filter(i => i !== '') @@ -116,6 +115,52 @@ export default { } }; + const geneCheckResults = ref({ + published: [], + notPublished: [], + notFound: [] + }); + const checkGeneStatusLoading = ref(false); + const activeTab = ref('published'); + + const checkGenesStatus = async () => { + const genes = genesAsText.value + ? genesAsText.value.split(/[, \n]/).filter(i => i.trim() !== '') + : []; + + if (genes.length === 0) return; + + checkGeneStatusLoading.value = true; + geneCheckResults.value = { + published: [], + notPublished: [], + notFound: [] + }; + + try { + const response = await api.post('/api/genes/check-genes', { + gene_symbol: genes.join(', '), + }); + + const resultsMap = Object.fromEntries(response.data.data.map(c => [c.gene_symbol, c])); + + genes.forEach(symbol => { + const result = resultsMap[symbol]; + if (!result) { + geneCheckResults.value.notFound.push(symbol); + } else if (result.current_status === 'Published') { + geneCheckResults.value.published.push(result); + } else { + geneCheckResults.value.notPublished.push(result); + } + }); + + } catch { + store.commit('pushError', 'Failed to check gene status.'); + } finally { + checkGeneStatusLoading.value = false; + } + }; watch(() => store.getters['groups/currentItem'], (to, from) => { if (to.id && (!from || to.id !== from.id)) { @@ -142,6 +187,10 @@ export default { cancel, syncGenesAsText, save, + geneCheckResults, + checkGeneStatusLoading, + checkGenesStatus, + activeTab, } }, computed: { @@ -164,13 +213,13 @@ export default {

Gene List -

- + +
+ +
+ +
+
+ + + +
+ +
+
+
+ + + + + + + + + + + + + + + + + +
GeneExpert PanelStatusStatus Date
{{ item.gene_symbol }}{{ item.expert_panel || 'Unknown Panel' }}{{ item.current_status || 'N/A' }}{{ item.current_status_date }}
+
+
+ +
+
+ + + + + + + + + + + + + + + + + +
GeneExpert PanelStatusStatus Date
{{ item.gene_symbol }}{{ item.expert_panel || 'Unknown Panel' }}{{ item.current_status || 'N/A' }}{{ item.current_status_date }}
+
+
+ +
+
+ {{ gene }} +
+
+
+

@@ -189,4 +326,4 @@ export default {

- \ No newline at end of file + diff --git a/routes/api.php b/routes/api.php index fa22d84b3..efcbde2e4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -102,6 +102,7 @@ Route::get('/diseases/search', [DiseaseLookupController::class, 'search']); Route::get('/diseases/{mondo_id}', [DiseaseLookupController::class, 'show']); +Route::post('/genes/check-genes', [GeneLookupController::class, 'check']); Route::get('/genes/search', [GeneLookupController::class, 'search']); Route::get('/genes/{hgnc_id}', [GeneLookupController::class, 'show']); From 94e25ee1476db05f83194e68b7c29d676e900d55 Mon Sep 17 00:00:00 2001 From: setiadha-unc Date: Thu, 26 Jun 2025 11:17:33 -0400 Subject: [PATCH 3/7] bulk upload list of genes to GT --- app/Services/GtApi/GtApiService.php | 7 ++++++- documentation/genetracker-integration.md | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/Services/GtApi/GtApiService.php b/app/Services/GtApi/GtApiService.php index b00258271..1b3644d3d 100644 --- a/app/Services/GtApi/GtApiService.php +++ b/app/Services/GtApi/GtApiService.php @@ -52,5 +52,10 @@ public function lookupGenesBulk(string $genes): array $response = $this->client->post('/genes/curations', ['gene_symbol' => $genes]); return $response->json(); } - + + public function approvalBulkUpload(array $payload): array + { + $response = $this->client->post('/genes/bulkupload', $payload); + return $response->json(); + } } diff --git a/documentation/genetracker-integration.md b/documentation/genetracker-integration.md index 0e2c93440..b4cbbd946 100644 --- a/documentation/genetracker-integration.md +++ b/documentation/genetracker-integration.md @@ -1,6 +1,27 @@ # TODO- this needs more clarification - Right now, the GPM directly accessess the mysql database of the genetracker. This is not optimal from the standpoint of isolation/security, and also makes dev setup/mocking a bit more complicated. Ideally the genetracker would have an api to abstract this away... +We are currently implementing a major architectural change to transition from direct database access between the GPM and GeneTracker (GT) Laravel applications to a more secure and scalable API-based communication. This change is being tracked under the following tickets: CGSP-755, GPM-500, and GT-70. +--- +### GT-70: Set Up API Server on GeneTracker +This task involves configuring GeneTracker as an API server using Laravel Passport with the Client Credentials Grant. The implementation follows a machine-to-machine pattern. +On the GPM side, the necessary credentials (Client ID and Client Secret) are stored in the .env file and accessed via Laravel’s config helper through the config('clientapi') configuration. +--- +### GPM-500: Configure GPM as API Client +This task focuses on enabling GPM to act as an API client to GeneTracker. Key components include: +- Token Management +The AccessTokenManager handles OAuth2 token retrieval and caching. +- API Services +Request logic is encapsulated under the App\Services\GtApi\ namespace, primarily within the GtApiService class. +To call the API, developers can use App\Services\GtApi\GtApiService and invoke the desired service method. Currently, this is built exclusively for GeneTracker integration, but the design allows for expansion to support additional services or APIs. Future enhancements could include modularizing services further and extending the token manager for multiple machine-to-machine integrations. +This task also involves replacing existing direct database queries from GPM to GeneTracker with equivalent API calls. +--- +### CGSP-755: UI-Level Integration +This is the parent ticket that oversees the overall integration. Its current scope includes UI-level features, such as: +- Sending curated gene data after approval +- Posting lists of genes to GeneTracker +More integrations may be added as this work evolves. + +### CGSP-2: look up in Gene Curation GCEP/VCEP to GT via api \ No newline at end of file From f07757fc296c9f426ae9f4f12c269ff8dc9de17d Mon Sep 17 00:00:00 2001 From: setiadha Date: Sun, 29 Jun 2025 17:23:05 -0400 Subject: [PATCH 4/7] refactor api --- .../Api/DiseaseLookupController.php | 2 +- .../Controllers/Api/GeneLookupController.php | 2 +- app/Providers/ApiServiceProvider.php | 42 +++++++++++++++++++ .../{GtApi => Api}/AccessTokenManager.php | 23 +++++----- .../GtApiClient.php => Api/ApiClient.php} | 14 +++---- app/Services/{GtApi => Api}/GtApiService.php | 6 +-- app/Services/DiseaseLookup.php | 2 +- app/Services/HgncLookup.php | 2 +- config/app.php | 2 +- config/services.php | 7 ++++ documentation/genetracker-integration.md | 2 +- 11 files changed, 75 insertions(+), 29 deletions(-) create mode 100644 app/Providers/ApiServiceProvider.php rename app/Services/{GtApi => Api}/AccessTokenManager.php (54%) rename app/Services/{GtApi/GtApiClient.php => Api/ApiClient.php} (67%) rename app/Services/{GtApi => Api}/GtApiService.php (93%) diff --git a/app/Http/Controllers/Api/DiseaseLookupController.php b/app/Http/Controllers/Api/DiseaseLookupController.php index 36bc7daa3..dcd5ca6b1 100644 --- a/app/Http/Controllers/Api/DiseaseLookupController.php +++ b/app/Http/Controllers/Api/DiseaseLookupController.php @@ -8,7 +8,7 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; -use App\Services\GtApi\GtApiService; +use App\Services\Api\GtApiService; class DiseaseLookupController extends Controller { diff --git a/app/Http/Controllers/Api/GeneLookupController.php b/app/Http/Controllers/Api/GeneLookupController.php index a912402c3..2393d6f3c 100644 --- a/app/Http/Controllers/Api/GeneLookupController.php +++ b/app/Http/Controllers/Api/GeneLookupController.php @@ -6,7 +6,7 @@ use Illuminate\Support\Facades\DB; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Log; -use App\Services\GtApi\GtApiService; +use App\Services\Api\GtApiService; class GeneLookupController extends Controller { diff --git a/app/Providers/ApiServiceProvider.php b/app/Providers/ApiServiceProvider.php new file mode 100644 index 000000000..c62c31d0f --- /dev/null +++ b/app/Providers/ApiServiceProvider.php @@ -0,0 +1,42 @@ +app->singleton(GtApiService::class, function () { + Log::info(config('services.gt_api')); + $config = config('services.gt_api'); + + $tokenManager = new AccessTokenManager([ + 'client_id' => $config['client_id'], + 'client_secret' => $config['client_secret'], + 'oauth_url' => $config['oauth_url'], + 'cache_key' => 'gt_api_access_token', + ]); + + $client = new ApiClient($config['base_url'], $tokenManager); + return new GtApiService($client); + }); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/app/Services/GtApi/AccessTokenManager.php b/app/Services/Api/AccessTokenManager.php similarity index 54% rename from app/Services/GtApi/AccessTokenManager.php rename to app/Services/Api/AccessTokenManager.php index 4f9a2c5ec..e6786ee61 100644 --- a/app/Services/GtApi/AccessTokenManager.php +++ b/app/Services/Api/AccessTokenManager.php @@ -1,7 +1,7 @@ clientId = config('services.gt_api.client_id'); - $this->clientSecret = config('services.gt_api.client_secret'); - $this->tokenUrl = config('services.gt_api.oauth_url'); + $this->clientId = $config['client_id']; + $this->clientSecret = $config['client_secret']; + $this->tokenUrl = $config['oauth_url']; + $this->cacheKey = $config['cache_key'] ?? md5($this->tokenUrl . $this->clientId); } public function getToken(): string { - // Check cache first - return Cache::remember('gt_api_access_token', 150, function () { + return Cache::remember($this->cacheKey, 150, function () { $response = Http::asForm()->post($this->tokenUrl, [ 'grant_type' => 'client_credentials', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, - 'scope' => '', + 'scope' => '', ]); if ($response->failed()) { - throw new \Exception('Failed to retrieve access token from GT API: ' . $response->body()); + throw new \Exception('Failed to retrieve access token: ' . $response->body()); } $data = $response->json(); - // Cache for slightly less than expires_in (default 180s) - Cache::put('gt_api_access_token', $data['access_token'], now()->addSeconds($data['expires_in'] - 10)); - + Cache::put($this->cacheKey, $data['access_token'], now()->addSeconds($data['expires_in'] - 10)); return $data['access_token']; }); } diff --git a/app/Services/GtApi/GtApiClient.php b/app/Services/Api/ApiClient.php similarity index 67% rename from app/Services/GtApi/GtApiClient.php rename to app/Services/Api/ApiClient.php index 11d71aba7..1673e8109 100644 --- a/app/Services/GtApi/GtApiClient.php +++ b/app/Services/Api/ApiClient.php @@ -1,29 +1,27 @@ baseUrl = config('services.gt_api.base_url'); + $this->baseUrl = rtrim($baseUrl, '/'); $this->tokenManager = $tokenManager; } protected function request(): \Illuminate\Http\Client\PendingRequest { - $accessToken = $this->tokenManager->getToken(); - return Http::timeout(10) ->retry(2, 200) - ->withToken($accessToken) + ->withToken($this->tokenManager->getToken()) ->acceptJson(); } @@ -35,7 +33,7 @@ public function post(string $endpoint, array $payload = []): Response ->throw(); } catch (RequestException $e) { report($e); - throw new \Exception('GT API request failed: ' . $e->getMessage()); + throw new \Exception('API request failed: ' . $e->getMessage()); } } } diff --git a/app/Services/GtApi/GtApiService.php b/app/Services/Api/GtApiService.php similarity index 93% rename from app/Services/GtApi/GtApiService.php rename to app/Services/Api/GtApiService.php index 1b3644d3d..c748e60fb 100644 --- a/app/Services/GtApi/GtApiService.php +++ b/app/Services/Api/GtApiService.php @@ -1,12 +1,12 @@ client = $client; } diff --git a/app/Services/DiseaseLookup.php b/app/Services/DiseaseLookup.php index c27a29766..c5aeb58a9 100644 --- a/app/Services/DiseaseLookup.php +++ b/app/Services/DiseaseLookup.php @@ -3,7 +3,7 @@ use App\Services\DiseaseLookupInterface; use Exception; -use App\Services\GtApi\GtApiService; +use App\Services\Api\GtApiService; class DiseaseLookup implements DiseaseLookupInterface { diff --git a/app/Services/HgncLookup.php b/app/Services/HgncLookup.php index a9131ac45..17decfca5 100644 --- a/app/Services/HgncLookup.php +++ b/app/Services/HgncLookup.php @@ -3,7 +3,7 @@ use Exception; use App\Services\HgncLookupInterface; -use App\Services\GtApi\GtApiService; +use App\Services\Api\GtApiService; class HgncLookup implements HgncLookupInterface { diff --git a/config/app.php b/config/app.php index fa2c66cc4..889d46254 100644 --- a/config/app.php +++ b/config/app.php @@ -59,7 +59,7 @@ App\Providers\EventServiceProvider::class, App\Providers\FortifyServiceProvider::class, App\Providers\RouteServiceProvider::class, - + App\Providers\ApiServiceProvider::class, /** * Module Providers */ diff --git a/config/services.php b/config/services.php index 775d6682f..37c076b97 100644 --- a/config/services.php +++ b/config/services.php @@ -36,4 +36,11 @@ 'oauth_url' => env('GT_CLIENT_BASE_URL') . '/oauth/token', 'base_url' => env('GT_CLIENT_BASE_URL') . env('GT_CLIENT_API'), ], + + 'affiliation_api' => [ + 'base_url' => env('AFFILIATION_API_BASE_URL'), + 'client_id' => env('AFFILIATION_API_CLIENT_ID'), + 'client_secret' => env('AFFILIATION_API_CLIENT_SECRET'), + 'oauth_url' => env('AFFILIATION_API_OAUTH_URL'), + ], ]; diff --git a/documentation/genetracker-integration.md b/documentation/genetracker-integration.md index b4cbbd946..292b7e771 100644 --- a/documentation/genetracker-integration.md +++ b/documentation/genetracker-integration.md @@ -15,7 +15,7 @@ This task focuses on enabling GPM to act as an API client to GeneTracker. Key co The AccessTokenManager handles OAuth2 token retrieval and caching. - API Services Request logic is encapsulated under the App\Services\GtApi\ namespace, primarily within the GtApiService class. -To call the API, developers can use App\Services\GtApi\GtApiService and invoke the desired service method. Currently, this is built exclusively for GeneTracker integration, but the design allows for expansion to support additional services or APIs. Future enhancements could include modularizing services further and extending the token manager for multiple machine-to-machine integrations. +To call the API, developers can use App\Services\Api\GtApiService and invoke the desired service method. Currently, this is built exclusively for GeneTracker integration, but the design allows for expansion to support additional services or APIs. Future enhancements could include modularizing services further and extending the token manager for multiple machine-to-machine integrations. This task also involves replacing existing direct database queries from GPM to GeneTracker with equivalent API calls. --- ### CGSP-755: UI-Level Integration From 06a03961c46761fc865ab34139348cf59ff6e3dd Mon Sep 17 00:00:00 2001 From: setiadha Date: Sun, 29 Jun 2025 17:30:57 -0400 Subject: [PATCH 5/7] remote log --- app/Providers/ApiServiceProvider.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/Providers/ApiServiceProvider.php b/app/Providers/ApiServiceProvider.php index c62c31d0f..73ddff7d8 100644 --- a/app/Providers/ApiServiceProvider.php +++ b/app/Providers/ApiServiceProvider.php @@ -15,9 +15,7 @@ class ApiServiceProvider extends ServiceProvider */ public function register(): void { - Log::info('ApiServiceProvider loaded!'); - $this->app->singleton(GtApiService::class, function () { - Log::info(config('services.gt_api')); + $this->app->singleton(GtApiService::class, function () { $config = config('services.gt_api'); $tokenManager = new AccessTokenManager([ From f83cce9fb2e06891312be59667f894436c8ae86d Mon Sep 17 00:00:00 2001 From: setiadha Date: Wed, 2 Jul 2025 12:20:52 -0400 Subject: [PATCH 6/7] init no DB direct access --- app/Actions/ReportVcepGenesMake.php | 14 ++++++ app/DataTransferObjects/GtDiseaseDto.php | 25 +++++++++++ app/DataTransferObjects/GtGeneDto.php | 26 +++++++++++ app/Modules/ExpertPanel/Models/Gene.php | 45 ++++++++++--------- .../Controllers/Api/GeneListController.php | 1 - 5 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 app/DataTransferObjects/GtDiseaseDto.php create mode 100644 app/DataTransferObjects/GtGeneDto.php diff --git a/app/Actions/ReportVcepGenesMake.php b/app/Actions/ReportVcepGenesMake.php index f5b25bd10..a72778192 100644 --- a/app/Actions/ReportVcepGenesMake.php +++ b/app/Actions/ReportVcepGenesMake.php @@ -60,6 +60,20 @@ private function pullData(): array 'expertPanel.group.type' ]) ->get(); + $gtApi = app(\App\Services\GtApi\GtApiService::class); + + $genes->each(function ($gene) use ($gtApi) { + try { + $disease = $gtApi->getDiseaseByMondoId($gene->mondo_id); + $gene->setRelation('disease', (object)[ + 'mondo_id' => $gene->mondo_id, + 'name' => $disease['data']['name'] ?? null, + ]); + } catch (\Throwable $e) { + $gene->setRelation('disease', (object) [] ); + Log::warning("Disease not found for MONDO ID: {$gene->mondo_id}"); + } + }); return $genes ->groupBy(function ($g) { diff --git a/app/DataTransferObjects/GtDiseaseDto.php b/app/DataTransferObjects/GtDiseaseDto.php new file mode 100644 index 000000000..0c1e63d70 --- /dev/null +++ b/app/DataTransferObjects/GtDiseaseDto.php @@ -0,0 +1,25 @@ +belongsTo(ExpertPanel::class); } - /** - * Get the gene that owns the Gene - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function gene(): BelongsTo + public function gene(): ?GtGeneDto { - return $this->belongsTo(GtGene::class, 'hgnc_id', 'hgnc_id'); + try { + return Cache::remember("hgnc_id_{$this->hgnc_id}", 300, function () { + $data = app(GtApiService::class)->getGeneSymbolById($this->hgnc_id)['data'] ?? null; + return $data ? GtGeneDto::fromArray($data) : null; + }); + } catch (\Throwable $e) { + report($e); + return null; + } } - /** - * Get the disease that owns the Gene - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function disease(): BelongsTo + public function disease(): ?GtDiseaseDto { - return $this->belongsTo(GtDisease::class, 'mondo_id', 'mondo_id'); + try { + return Cache::remember("mondo_id_{$this->mondo_id}", 300, function () { + $data = app(GtApiService::class)->getDiseaseByMondoId($this->mondo_id)['data'] ?? null; + return $data ? GtDiseaseDto::fromArray($data) : null; + }); + } catch (\Throwable $e) { + report($e); + return null; + } } /** diff --git a/app/Modules/Group/Http/Controllers/Api/GeneListController.php b/app/Modules/Group/Http/Controllers/Api/GeneListController.php index ffcf35132..faea59df5 100644 --- a/app/Modules/Group/Http/Controllers/Api/GeneListController.php +++ b/app/Modules/Group/Http/Controllers/Api/GeneListController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use App\Modules\Group\Models\Group; use App\Http\Controllers\Controller; -use App\Models\GeneTracker\Disease; use Illuminate\Database\Eloquent\ModelNotFoundException; class GeneListController extends Controller From 606f4c3bdea970bc1f5f2db7ac26deaa5f8f7bc7 Mon Sep 17 00:00:00 2001 From: setiadha Date: Thu, 3 Jul 2025 11:36:02 -0400 Subject: [PATCH 7/7] refactor rules --- app/Actions/ReportVcepGenesMake.php | 28 ++++++++-------- app/Models/GeneTracker/Disease.php | 20 ----------- app/Models/GeneTracker/Gene.php | 33 ------------------- app/Modules/Group/Actions/GeneUpdate.php | 24 ++++++++++---- app/Modules/Group/Actions/GenesAdd.php | 10 +++--- app/Modules/Group/Actions/GenesAddToVcep.php | 33 +++++++++++++++---- app/Modules/Group/Actions/GenesSyncToGcep.php | 27 +++++++++++---- app/Services/Api/GtApiService.php | 6 ++++ 8 files changed, 90 insertions(+), 91 deletions(-) delete mode 100644 app/Models/GeneTracker/Disease.php delete mode 100644 app/Models/GeneTracker/Gene.php diff --git a/app/Actions/ReportVcepGenesMake.php b/app/Actions/ReportVcepGenesMake.php index a72778192..e453cc780 100644 --- a/app/Actions/ReportVcepGenesMake.php +++ b/app/Actions/ReportVcepGenesMake.php @@ -47,9 +47,6 @@ private function pullData(): array }) ->orderBy('gene_symbol') ->with([ - 'disease' => function ($q) { - $q->select(['mondo_id','name']); - }, 'expertPanel' => function ($q) { $q->select(['id', 'long_base_name', 'expert_panel_type_id']); }, @@ -60,19 +57,20 @@ private function pullData(): array 'expertPanel.group.type' ]) ->get(); - $gtApi = app(\App\Services\GtApi\GtApiService::class); + $gtApi = app(\App\Services\Api\GtApiService::class); + + $mondoIds = $genes->pluck('mondo_id')->unique()->values()->all(); + $diseaseData = $gtApi->getDiseasesByMondoIds($mondoIds); - $genes->each(function ($gene) use ($gtApi) { - try { - $disease = $gtApi->getDiseaseByMondoId($gene->mondo_id); - $gene->setRelation('disease', (object)[ - 'mondo_id' => $gene->mondo_id, - 'name' => $disease['data']['name'] ?? null, - ]); - } catch (\Throwable $e) { - $gene->setRelation('disease', (object) [] ); - Log::warning("Disease not found for MONDO ID: {$gene->mondo_id}"); - } + // dd($diseaseData); + $diseaseMap = collect($diseaseData['data']) + ->keyBy('mondo_id') + ->map(fn($d) => (object)[ + 'mondo_id' => $d['mondo_id'], + 'name' => $d['name'] + ]); + $genes->each(function ($gene) use ($diseaseMap) { + $gene->setRelation('disease', $diseaseMap[$gene->mondo_id] ?? (object)[]); }); return $genes diff --git a/app/Models/GeneTracker/Disease.php b/app/Models/GeneTracker/Disease.php deleted file mode 100644 index e51f759ec..000000000 --- a/app/Models/GeneTracker/Disease.php +++ /dev/null @@ -1,20 +0,0 @@ -hasMany(GpmGene::class, 'foreign_key', 'local_key'); - } -} diff --git a/app/Modules/Group/Actions/GeneUpdate.php b/app/Modules/Group/Actions/GeneUpdate.php index 89ce7f6ec..5f15f1387 100644 --- a/app/Modules/Group/Actions/GeneUpdate.php +++ b/app/Modules/Group/Actions/GeneUpdate.php @@ -28,8 +28,21 @@ public function __construct(private HgncLookupInterface $hgncLookup, private Dis public function handle(Group $group, Gene $gene, array $data): Group { - $data['gene_symbol'] = $this->hgncLookup->findSymbolById($data['hgnc_id']); - $data['disease_name'] = $this->mondoLookup->findNameByOntologyId($data['mondo_id']); + try { + $data['gene_symbol'] = $this->hgncLookup->findSymbolById($data['hgnc_id']); + } catch (\Throwable $e) { + throw \Illuminate\Validation\ValidationException::withMessages([ + 'hgnc_id' => 'HGNC ID not found or invalid.', + ]); + } + + try { + $data['disease_name'] = $this->mondoLookup->findNameByOntologyId($data['mondo_id']); + } catch (\Throwable $e) { + throw \Illuminate\Validation\ValidationException::withMessages([ + 'mondo_id' => 'MONDO ID not found or invalid.', + ]); + } $gene->update($data); return $group; @@ -61,15 +74,14 @@ public function authorize(ActionRequest $request, Group $group): bool public function rules(ActionRequest $request): array - { - $connectionName = config('database.gt_db_connection'); + { $rules = [ - 'hgnc_id' => 'required|numeric|exists:'.$connectionName.'.genes,hgnc_id', + 'hgnc_id' => 'required|numeric', ]; $group = $request->group; if ($group->isVcepOrScvcep) { - $rules['mondo_id'] = 'required|regex:/MONDO:\d\d\d\d\d\d\d/i|exists:'.$connectionName.'.diseases,mondo_id'; + $rules['mondo_id'] = 'required|regex:/MONDO:\d{7}/i'; } return $rules; diff --git a/app/Modules/Group/Actions/GenesAdd.php b/app/Modules/Group/Actions/GenesAdd.php index 615674fff..075deff98 100644 --- a/app/Modules/Group/Actions/GenesAdd.php +++ b/app/Modules/Group/Actions/GenesAdd.php @@ -47,20 +47,20 @@ public function authorize(ActionRequest $request): bool } public function rules(ActionRequest $request): array - { - $gtConn = config('database.gt_db_connection'); + { $group = $request->group; if ($group->isVcepOrScvcep) { return [ 'genes' => 'required|array|min:1', 'genes.*' => 'required|array:hgnc_id,mondo_id', - 'genes.*.hgnc_id' => 'required|numeric|exists:'.$gtConn.'.genes,hgnc_id', - 'genes.*.mondo_id' => 'required|regex:/MONDO:\d\d\d\d\d\d\d/i|exists:'.$gtConn.'.diseases,mondo_id' + 'genes.*.hgnc_id' => 'required|numeric', + 'genes.*.mondo_id' => 'required|regex:/MONDO:\d{7}/i' ]; } if ($group->isGcep) { return [ - 'genes.*' => 'exists:'.$gtConn.'.genes,gene_symbol' + 'genes' => 'required|array|min:1', + 'genes.*' => 'required|string' ]; } diff --git a/app/Modules/Group/Actions/GenesAddToVcep.php b/app/Modules/Group/Actions/GenesAddToVcep.php index 2072f1925..f4785a5a0 100644 --- a/app/Modules/Group/Actions/GenesAddToVcep.php +++ b/app/Modules/Group/Actions/GenesAddToVcep.php @@ -30,14 +30,35 @@ public function handle(Group $group, array $genes): Group throw ValidationException::withMessages(['group' => 'The group is not a VCEP.']); } - $genes = collect(array_map(function ($gene) { - return new Gene([ + $genes = collect(); + + foreach ($inputGenes as $index => $gene) { + try { + $geneSymbol = $this->hgncLookup->findSymbolById($gene['hgnc_id']); + } catch (\Throwable $e) { + throw ValidationException::withMessages([ + "genes.$index.hgnc_id" => "HGNC ID {$gene['hgnc_id']} not found or invalid.", + ]); + } + + try { + $diseaseName = $this->mondoLookup->findNameByOntologyId($gene['mondo_id']); + } catch (\Throwable $e) { + throw ValidationException::withMessages([ + "genes.$index.mondo_id" => "MONDO ID {$gene['mondo_id']} not found or invalid.", + ]); + } + + $genes->push(new Gene([ 'hgnc_id' => $gene['hgnc_id'], - 'gene_symbol' => $this->hgncLookup->findSymbolById($gene['hgnc_id']), + 'gene_symbol' => $geneSymbol, 'mondo_id' => $gene['mondo_id'], - 'disease_name' => $this->mondoLookup->findNameByOntologyId($gene['mondo_id']) - ]); - }, $genes)); + 'disease_name' => $diseaseName, + ])); + } + if ($genes->isEmpty()) { + throw new ValidationException('No valid genes provided for addition.'); + } $group->expertPanel->genes()->saveMany($genes); event(new GenesAdded($group, $genes)); diff --git a/app/Modules/Group/Actions/GenesSyncToGcep.php b/app/Modules/Group/Actions/GenesSyncToGcep.php index c3e0fd74a..3a2ddd392 100644 --- a/app/Modules/Group/Actions/GenesSyncToGcep.php +++ b/app/Modules/Group/Actions/GenesSyncToGcep.php @@ -49,12 +49,27 @@ private function removeGenes($group, $removedGeneSymbols) private function addNewGenes($group, $addedGeneSymbols) { if ($addedGeneSymbols->count() > 0) { - $genes = $addedGeneSymbols->map(function ($gs) { - return new Gene([ - 'hgnc_id' => $this->hgncLookup->findHgncIdBySymbol($gs), - 'gene_symbol' => $gs - ]); - }); + + $genes = collect(); + + foreach ($addedGeneSymbols as $index => $geneSymbol) { + try { + $hgncId = $this->hgncLookup->findHgncIdBySymbol($geneSymbol); + } catch (\Throwable $e) { + throw ValidationException::withMessages([ + "genes.$index" => "Gene symbol '{$geneSymbol}' not found or invalid.", + ]); + } + + $genes->push(new Gene([ + 'hgnc_id' => $hgncId, + 'gene_symbol' => $geneSymbol, + ])); + } + if ($genes->isEmpty()) { + throw new ValidationException('No valid genes provided for addition.'); + } + $group->expertPanel->genes()->saveMany($genes); event(new GenesAdded($group, $genes)); } diff --git a/app/Services/Api/GtApiService.php b/app/Services/Api/GtApiService.php index c748e60fb..b4b98a9c8 100644 --- a/app/Services/Api/GtApiService.php +++ b/app/Services/Api/GtApiService.php @@ -41,6 +41,12 @@ public function getDiseaseByMondoId(string $mondoId): array return $response->json(); } + public function getDiseasesByMondoIds(array $mondoId): array + { + $response = $this->client->post('/diseases/mondos', ['mondo_ids' => $mondoId]); + return $response->json(); + } + public function getDiseaseByOntologyId(string $ontologyId): array { $response = $this->client->post('/diseases/ontology', ['ontology_id' => $ontologyId]);