Skip to content

Commit

Permalink
Merge pull request #80 from art-institute-of-chicago/feature/csv-search
Browse files Browse the repository at this point in the history
Add CSV search endpoonts [API-398]
  • Loading branch information
nikhiltri authored Dec 13, 2023
2 parents 12562af + 4db9f54 commit b405b4e
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 4 deletions.
68 changes: 68 additions & 0 deletions app/Http/Controllers/CsvSearchController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace App\Http\Controllers;

use Aic\Hub\Foundation\Exceptions\DetailedException;
use App\Http\Search\Request as SearchRequest;
use App\Http\Search\CsvResponse as SearchResponse;
use Illuminate\Support\Facades\Request as RequestFacade;
use Illuminate\Http\Request;
use Elasticsearch;

class CsvSearchController extends SearchController
{
public function search(Request $request, $resource = null)
{
$headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'inline',
];
$data = $this->query('getSearchParams', 'getSearchResponse', 'search', $resource);
$titles = array_keys($data[0]);

$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;
}
}
41 changes: 41 additions & 0 deletions app/Http/Search/CsvResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace App\Http\Search;

use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Request as RequestFacade;

class CsvResponse extends Response
{
/**
* Transform response for search queries.
*
* @return array
*/
public function getSearchResponse()
{
// Strip off extraneous search fields to simplify output
$except = ['_score', 'thumbnail'];

return collect($this->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'];
}
}
8 changes: 4 additions & 4 deletions app/Http/Search/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -181,7 +181,7 @@ private function paginate()
*
* @return array
*/
private function data()
protected function data()
{
$hits = $this->searchResponse['hits']['hits'];
$results = [];
Expand Down Expand Up @@ -224,7 +224,7 @@ private function data()
];
}

private function info()
protected function info()
{
$resources = $this->getResources();

Expand Down Expand Up @@ -283,7 +283,7 @@ private function getAutocompleteWithTitle()
*
* @return array
*/
private function aggregate()
protected function aggregate()
{
$aggregations = $this->searchResponse['aggregations'] ?? null;

Expand Down
4 changes: 4 additions & 0 deletions app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
Expand Down
43 changes: 43 additions & 0 deletions routes/csv.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ResourceController;
use App\Http\Controllers\RestrictedResourceController;
use App\Http\Controllers\CsvSearchController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

app('url')->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')]);
}
});

0 comments on commit b405b4e

Please sign in to comment.