diff --git a/app/Actions/ReportVcepGenesMake.php b/app/Actions/ReportVcepGenesMake.php index f5b25bd10..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,6 +57,21 @@ private function pullData(): array 'expertPanel.group.type' ]) ->get(); + $gtApi = app(\App\Services\Api\GtApiService::class); + + $mondoIds = $genes->pluck('mondo_id')->unique()->values()->all(); + $diseaseData = $gtApi->getDiseasesByMondoIds($mondoIds); + + // 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 ->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 @@ +gtApi = $gtApi; + } + public function show($mondoId) { $validator = Validator::make(['mondo_id' => $mondoId], [ @@ -19,24 +28,46 @@ 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']); + try { + $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.', + '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 { + $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.', + 'details' => $e->getMessage(), + ], 500); + } } } diff --git a/app/Http/Controllers/Api/GeneLookupController.php b/app/Http/Controllers/Api/GeneLookupController.php index 630d4571c..2393d6f3c 100644 --- a/app/Http/Controllers/Api/GeneLookupController.php +++ b/app/Http/Controllers/Api/GeneLookupController.php @@ -5,16 +5,18 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use App\Http\Controllers\Controller; +use Illuminate\Support\Facades\Log; +use App\Services\Api\GtApiService; 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 ?? '')); @@ -22,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); + + 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); + } + } - return $results->toArray(); + 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/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/ExpertPanel/Models/Gene.php b/app/Modules/ExpertPanel/Models/Gene.php index c7b96a408..af3e7043f 100644 --- a/app/Modules/ExpertPanel/Models/Gene.php +++ b/app/Modules/ExpertPanel/Models/Gene.php @@ -3,13 +3,14 @@ namespace App\Modules\ExpertPanel\Models; use Illuminate\Database\Eloquent\Model; -use App\Models\GeneTracker\Gene as GtGene; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Models\GeneTracker\Disease as GtDisease; use Database\Factories\GeneFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Factories\HasFactory; +use App\DataTransferObjects\GtGeneDto; +use App\DataTransferObjects\GtDiseaseDto; + /** * @property int $id * @property int $hgnc_id @@ -25,12 +26,6 @@ class Gene extends Model { use HasFactory, SoftDeletes; - public function getConnectionName() - { - return config('database.default'); - } - - /** * The attributes that are mass assignable. * @@ -65,24 +60,30 @@ public function expertPanel() return $this->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/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/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 diff --git a/app/Providers/ApiServiceProvider.php b/app/Providers/ApiServiceProvider.php new file mode 100644 index 000000000..73ddff7d8 --- /dev/null +++ b/app/Providers/ApiServiceProvider.php @@ -0,0 +1,40 @@ +app->singleton(GtApiService::class, function () { + $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/Api/AccessTokenManager.php b/app/Services/Api/AccessTokenManager.php new file mode 100644 index 000000000..e6786ee61 --- /dev/null +++ b/app/Services/Api/AccessTokenManager.php @@ -0,0 +1,44 @@ +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 + { + 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' => '', + ]); + + if ($response->failed()) { + throw new \Exception('Failed to retrieve access token: ' . $response->body()); + } + + $data = $response->json(); + Cache::put($this->cacheKey, $data['access_token'], now()->addSeconds($data['expires_in'] - 10)); + return $data['access_token']; + }); + } +} diff --git a/app/Services/Api/ApiClient.php b/app/Services/Api/ApiClient.php new file mode 100644 index 000000000..1673e8109 --- /dev/null +++ b/app/Services/Api/ApiClient.php @@ -0,0 +1,39 @@ +baseUrl = rtrim($baseUrl, '/'); + $this->tokenManager = $tokenManager; + } + + protected function request(): \Illuminate\Http\Client\PendingRequest + { + return Http::timeout(10) + ->retry(2, 200) + ->withToken($this->tokenManager->getToken()) + ->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('API request failed: ' . $e->getMessage()); + } + } +} diff --git a/app/Services/Api/GtApiService.php b/app/Services/Api/GtApiService.php new file mode 100644 index 000000000..b4b98a9c8 --- /dev/null +++ b/app/Services/Api/GtApiService.php @@ -0,0 +1,67 @@ +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 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]); + return $response->json(); + } + + 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/app/Services/DiseaseLookup.php b/app/Services/DiseaseLookup.php index df49dd624..c5aeb58a9 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\Api\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/HgncLookup.php b/app/Services/HgncLookup.php index c25c7a3cc..17decfca5 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\Api\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/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 2a1d616c7..37c076b97 100644 --- a/config/services.php +++ b/config/services.php @@ -30,4 +30,17 @@ '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'), + ], + + '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 0e2c93440..292b7e771 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\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 +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 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 | +Expert Panel | +Status | +Status Date | +
|---|---|---|---|
| {{ item.gene_symbol }} | +{{ item.expert_panel || 'Unknown Panel' }} | +{{ item.current_status || 'N/A' }} | +{{ item.current_status_date }} | +
| Gene | +Expert Panel | +Status | +Status Date | +
|---|---|---|---|
| {{ item.gene_symbol }} | +{{ item.expert_panel || 'Unknown Panel' }} | +{{ item.current_status || 'N/A' }} | +{{ item.current_status_date }} | +
@@ -189,4 +326,4 @@ export default {