diff --git a/app/Http/Controllers/CsvSearchController.php b/app/Http/Controllers/CsvSearchController.php new file mode 100644 index 00000000..a55e6c01 --- /dev/null +++ b/app/Http/Controllers/CsvSearchController.php @@ -0,0 +1,72 @@ + 'text/csv', + 'Content-Disposition' => 'inline', + ]; + $data = $this->query('getSearchParams', 'getSearchResponse', 'search', $resource); + $titles = []; + foreach ($data[0] as $key => $value) { + $titles[] = $key; + } + + $callback = function () use ($data, $titles) { + $output = fopen('php://output', 'w'); + fputcsv($output, $titles); + foreach ($data as $row) { + fputcsv($output, $row); + } + fclose($output); + }; + return response()->stream($callback, 200, $headers); + } + + /** + * Helper method to perform a query against Elasticsearch endpoint. + * + * @param string $requestMethod Name of transformation method on SearchRequest class + * @param string $responseMethod Name of transformation method on SearchResponse class + * @param array $resource Resource to search (translates to index and type) + * @param string $id Identifier of a resource (meant for explain) + * + * @return \Illuminate\Http\Response + */ + protected function query($requestMethod, $responseMethod, $elasticsearchMethod, $resource, $id = null, $requestArgs = null) + { + // Combine any configuration params + $input = RequestFacade::all(); + $input = $requestArgs ? array_merge($input, $requestArgs) : $input; + + // Transform our API's syntax into an Elasticsearch params array + $params = ( new SearchRequest($resource, $id) )->{$requestMethod}($input); + $results = null; + + try { + $results = Elasticsearch::$elasticsearchMethod($params); + } catch (\Exception $e) { + // Elasticsearch occasionally returns a status code of zero + $code = $e->getCode() > 0 ? $e->getCode() : 500; + + return response($e->getMessage(), $code)->header('Content-Type', 'text/csv'); + } + + // Transform Elasticsearch results into our API standard + $response = ( new SearchResponse($results, $params, $resource) )->{$responseMethod}(); + + return $response; + } +} diff --git a/app/Http/Search/CsvResponse.php b/app/Http/Search/CsvResponse.php new file mode 100644 index 00000000..081c534e --- /dev/null +++ b/app/Http/Search/CsvResponse.php @@ -0,0 +1,41 @@ +data())->map(function ($item) use ($except) { + return collect($item)->except($except)->map(function ($field) { + // If the field is an array, smush it into one cell + if (is_array($field)) { + return implode(",", $field); + } + return $field; + })->all(); + })->toArray(); + } + + /** + * Add data (i.e. hits, results) to response. + * + * @return array + */ + protected function data() + { + return parent::data()['data']; + } +} diff --git a/app/Http/Search/Response.php b/app/Http/Search/Response.php index bfddcd74..6cd2561f 100644 --- a/app/Http/Search/Response.php +++ b/app/Http/Search/Response.php @@ -144,7 +144,7 @@ function ($item) { * * @return array */ - private function paginate() + protected function paginate() { // We assume that `size` and `from` have been set via getPaginationParams() // This method should not be used for endpoints that return no results @@ -181,7 +181,7 @@ private function paginate() * * @return array */ - private function data() + protected function data() { $hits = $this->searchResponse['hits']['hits']; $results = []; @@ -224,7 +224,7 @@ private function data() ]; } - private function info() + protected function info() { $resources = $this->getResources(); @@ -283,7 +283,7 @@ private function getAutocompleteWithTitle() * * @return array */ - private function aggregate() + protected function aggregate() { $aggregations = $this->searchResponse['aggregations'] ?? null; diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 58949982..ca8ecc95 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -37,6 +37,10 @@ public function boot(): void ->prefix('la') ->group(base_path('routes/la.php')); + Route::middleware('api') + ->prefix('csv') + ->group(base_path('routes/csv.php')); + Route::middleware('web') ->group(base_path('routes/web.php')); }); diff --git a/routes/csv.php b/routes/csv.php new file mode 100644 index 00000000..8dbb228c --- /dev/null +++ b/routes/csv.php @@ -0,0 +1,43 @@ +forceRootUrl(config('aic.proxy_url')); +app('url')->forceScheme(config('aic.proxy_scheme')); + +Route::group(['prefix' => 'v1'], function () { + // Elasticsearch + Route::match(['GET', 'POST'], 'search', [CsvSearchController::class, 'search']); + Route::match(['GET', 'POST'], '{resource}/search', [CsvSearchController::class, 'search']); + + // Define all of our resource routes by looping through config + foreach (config('resources.outbound.base') as $resource) { + if (!isset($resource['endpoint'])) { + continue; + } + + $isScoped = $resource['scope_of'] ?? false; + $isRestricted = $resource['is_restricted'] ?? false; + + $controller = $resource['controller'] ?? ( + ($isRestricted && env('APP_ENV') !== 'testing') ? RestrictedResourceController::class : ResourceController::class + ); + + Route::any($resource['endpoint'], [$controller, ($isScoped ? 'indexScope' : 'index')]); + Route::any($resource['endpoint'] . '/{id}', [$controller, ($isScoped ? 'showScope' : 'show')]); + } +});