diff --git a/app/config/routes.php b/app/config/routes.php index 173a3aa6d..31198f027 100644 --- a/app/config/routes.php +++ b/app/config/routes.php @@ -66,6 +66,7 @@ $routes->put('/api/:matchgrid_id/v1/people/:sor/:sorid', ['controller' => 'TierApi', 'action' => 'match']); $routes->post('/api/:matchgrid_id/v1/people/:sor/:sorid', ['controller' => 'TierApi', 'action' => 'search']); // This doesn't match and we end up in current(), so we just check there +// Also, as of API v1.0.0, search over GET has been removed // $routes->get('/api/:matchgrid_id/v1/people/:sor/:sorid?*', ['controller' => 'TierApi', 'action' => 'search']); $routes->delete('/api/:matchgrid_id/v1/people/:sor/:sorid', ['controller' => 'TierApi', 'action' => 'remove']); $routes->get('/api/:matchgrid_id/v1/people/:sor/:sorid', ['controller' => 'TierApi', 'action' => 'current']); diff --git a/app/src/Command/DatabaseCommand.php b/app/src/Command/DatabaseCommand.php index d12024832..c72238f6e 100644 --- a/app/src/Command/DatabaseCommand.php +++ b/app/src/Command/DatabaseCommand.php @@ -20,7 +20,7 @@ * limitations under the License. * * @link http://www.internet2.edu/comanage COmanage Project - * @package match + * @package common * @since COmanage Common v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ @@ -54,7 +54,7 @@ class DatabaseCommand extends Command { * @return ConsoleOptionParser ConsoleOptionParser */ - protected function buildOptionParser(ConsoleOptionParser $parser) { + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->addOption('not', [ 'short' => 'n', 'boolean' => true, @@ -67,7 +67,7 @@ protected function buildOptionParser(ConsoleOptionParser $parser) { /** * Execute the Database Command. * - * @since COmanage Match v1.0.0, COmanage Registry v5.0.0 + * @since COmanage Match v1.0.0, COmanage Registry v6.0.0 * @param Arguments $args Command Arguments * @param ConsoleIo $io Console IO * @throws RuntimeException @@ -93,6 +93,8 @@ public function execute(Arguments $args, ConsoleIo $io) { throw new \RuntimeException(__($COmponent.'.er.file', [$schemaFile])); } + $io->out(__($COmponent.'.cmd.db.schema', [$schemaFile])); + $json = file_get_contents($schemaFile); $schemaConfig = json_decode($json); @@ -231,8 +233,8 @@ public function execute(Arguments $args, ConsoleIo $io) { try { // We manually call compare so we can get the SchemaDiff object. We need - // this for toSaveSql(), which won't drop undocumented tables (like the - // matchgrids, which are dynamically created and so won't be in the + // this for toSaveSql(), which we use to avoid dropping undocumented tables + // (like the matchgrids, which are dynamically created and so won't be in the // schema file). // $diffSql = $curSchema->getMigrateToSql($schema, $conn->getDatabasePlatform()); $comparator = new Comparator(); @@ -245,12 +247,13 @@ public function execute(Arguments $args, ConsoleIo $io) { // needs to be fixed. foreach($diffSql as $sql) { + // XXX At some point do this only if $verbose $io->out($sql); if($cfg['driver'] == 'Cake\Database\Driver\Postgres' && preg_match("/^DROP SEQUENCE [a-z]*_id_seq/", $sql)) { // Remove the DROP SEQUENCE statements in $fromSql because they're Postgres automagic - // being misinterpretted. (Note toSaveSql might mask this now.) + // being misinterpreted. (Note toSaveSql might mask this now.) // XXX Maybe debug and file a PR to not emit DROP SEQUENCE on PG for autoincrementesque fields? $io->out("Skipping sequence drop"); } else { @@ -262,7 +265,9 @@ public function execute(Arguments $args, ConsoleIo $io) { } if(!$doSQL) { - $io->out('** SQL NOT EXECUTED **'); + $io->out(__($COmponent.'.cmd.db.noop')); + } else { + $io->out(__($COmponent.'.cmd.db.ok')); } } catch(\Exception $e) { diff --git a/app/src/Controller/TierApiController.php b/app/src/Controller/TierApiController.php index 04129afd8..4e42cb50c 100644 --- a/app/src/Controller/TierApiController.php +++ b/app/src/Controller/TierApiController.php @@ -81,6 +81,7 @@ public function current() { // This is actually a Search Only request via GET, but routes.php doesn't // quite want to send the request to search(). Regardless, we don't // (currently) support search over GET, so throw an error. + // And as of API v1.0.0, search over GET has been removed. throw new \Cake\Http\Exception\MethodNotAllowedException("GET not supported for Search Only, use POST instead"); } @@ -455,7 +456,7 @@ protected function doRemove(\App\Lib\Match\MatchService $MatchService) { } /** - * Handle an API Match Request request, ie: GET /v1/matchRequest/id + * Handle an API Match Request request, ie: GET /v1/matchRequests/id * * @since COmanage Match v1.0.0 * @param MatchService $MatchService Match Service @@ -474,7 +475,7 @@ protected function doViewMatchRequest(\App\Lib\Match\MatchService $MatchService) // Parse the original request $origReq = $results->getResultsForJson('current'); - if(!empty($origReq['referenceId'])) { + if(!empty($origReq['meta']['referenceId'])) { // This is a resolved request, so handle it a bit differently $this->statusCode = 200; $this->result = $origReq; @@ -487,16 +488,11 @@ protected function doViewMatchRequest(\App\Lib\Match\MatchService $MatchService) // the moment we don't try to catch that. $this->statusCode = 300; - // Extract the SOR and SORID - $sor = $origReq['sorAttributes']['sor']; - $sorid = null; + $this->result['matchRequest'] = $this->request->getParam('id'); - foreach($origReq['sorAttributes']['identifiers'] as $id) { - if($id['type'] == 'sor') { - $sorid = $id['identifier']; - break; - } - } + // Extract the SOR and SORID + $sor = $origReq['meta']['sorLabel']; + $sorid = $origReq['meta']['sorId']; // Use AttributeManager to parse the current record back into database format for searching $AttributeManager = new \App\Lib\Match\AttributeManager(); diff --git a/app/src/Lib/Match/MatchService.php b/app/src/Lib/Match/MatchService.php index 1ee5d7f74..e79af3661 100644 --- a/app/src/Lib/Match/MatchService.php +++ b/app/src/Lib/Match/MatchService.php @@ -136,7 +136,9 @@ public function getRequest(int $id) { FROM " . $this->mgTable . " WHERE id=?"; - $row = $this->dbc->GetRow($sql, [$id]); + $stmt = $this->dbc->Prepare($sql); + + $row = $this->dbc->GetRow($stmt, [$id]); if($row === false) { throw new \RuntimeException($this->dbc->errorMsg()); @@ -301,7 +303,9 @@ public function getSorAttributes(string $sor, string $sorid) { WHERE sor=? AND sorid=?"; - $attrs = $this->dbc->GetRow($sql, [$sor, $sorid]); + $stmt = $this->dbc->Prepare($sql); + + $attrs = $this->dbc->GetRow($stmt, [$sor, $sorid]); if($attrs === false) { throw new \RuntimeException($this->dbc->errorMsg()); @@ -326,7 +330,9 @@ public function getSorIds(string $sor) { FROM " . $this->mgTable . " WHERE sor=?"; - $sorids = $this->dbc->GetCol($sql, [$sor]); + $stmt = $this->dbc->Prepare($sql); + + $sorids = $this->dbc->GetCol($stmt, [$sor]); if($sorids === false) { throw new \RuntimeException($this->dbc->errorMsg()); @@ -446,8 +452,10 @@ public function remove(string $sor, string $sorid) { AND sorid=? RETURNING id"; // Postgres SQL Extension + $stmt = $this->dbc->Prepare($sql); + // This should only ever match zero or one rows - $ret = $this->dbc->GetOne($sql, [$sor, $sorid]); + $ret = $this->dbc->GetOne($stmt, [$sor, $sorid]); if($ret === false) { throw new \RuntimeException($this->dbc->errorMsg()); diff --git a/app/src/Lib/Match/ResultManager.php b/app/src/Lib/Match/ResultManager.php index 97d50785b..e2e220328 100644 --- a/app/src/Lib/Match/ResultManager.php +++ b/app/src/Lib/Match/ResultManager.php @@ -113,6 +113,56 @@ public function count() { return count($this->results); } + /** + * Filter Metadata attributes in preparation for the generation of a Match Result. + * + * @since COmanage Match v1.0.0 + * @param array $parsed Record attributes + * @param string $referenceId Reference ID, if known + * @return array Attributes parsed into 'meta' and 'sorAttributes' + */ + + protected function filterMetadata($parsed, $referenceId=null) { + $ret = array(); + + if($referenceId) { + $ret['meta']['referenceId'] = $referenceId; + } + + foreach(array_keys($parsed) as $attr) { + switch($attr) { + case 'matchRequest': + // Force to a string as per the spec (internally we use an int) + $ret['meta'][$attr] = (string)$parsed[$attr]; + break; + case 'request_time': + case 'resolution_time': + // Timestamps, format and inflect name + $ret['meta'][\Cake\Utility\Inflector::variable($attr)] = strftime("%FT%TZ", + strtotime($parsed[$attr])); + break; + case 'sor': + $ret['meta']['sorLabel'] = $parsed[$attr]; + break; + default: + // Any other attribute is not metadata, so just copy to sorAttributes + $ret['sorAttributes'][$attr] = $parsed[$attr]; + break; + } + } + + // If an SOR ID is present in an Identifier, copy it to the metadata + // (but leave the original where it was). + + $sorId = \Cake\Utility\Hash::extract($parsed, "identifiers.{n}[type=sor].identifier"); + + if(!empty($sorId)) { + $ret['meta']['sorId'] = $sorId[0]; + } + + return $ret; + } + /** * Get the Confidence Mode for this set of results. * @@ -159,7 +209,10 @@ public function getResultsForJson($mode="search") { foreach($this->results as $referenceId => $sorRow) { // Note $candidate is not used by mode=pending - $candidate = ['referenceId' => $referenceId]; + $candidate = [ + 'referenceId' => $referenceId, + 'sorRecords' => [] + ]; foreach($sorRow as $rowId => $attrs) { $parsed = ['matchRequest' => $rowId]; @@ -205,51 +258,18 @@ public function getResultsForJson($mode="search") { if($mode == 'current') { // There should only be one entry, so return it directly - $candidate['sorAttributes'] = $parsed; - - // Bump up request and resolution times - $candidate['requestTime'] = strftime("%FT%TZ", - strtotime($candidate['sorAttributes']['request_time'])); - - unset($candidate['sorAttributes']['request_time']); - - if(!empty($candidate['sorAttributes']['resolution_time'])) { - $candidate['resolutionTime'] = strftime("%FT%TZ", - strtotime($candidate['sorAttributes']['resolution_time'])); - } - unset($candidate['sorAttributes']['resolution_time']); - - return $candidate; + return $this->filterMetadata($parsed, $referenceId); } elseif($mode == 'pending') { - $ret[$rowId] = ['attributes' => $parsed]; - - // Bump up request and resolution times - $ret[$rowId]['requestTime'] = strftime("%FT%TZ", strtotime($parsed['request_time'])); - - unset($ret[$rowId]['attributes']['request_time']); - - if(!empty($parsed['resolution_time'])) { - $ret[$rowId]['resolutionTime'] = strftime("%FT%TZ", strtotime($parsed['resolution_time'])); - } - unset($ret[$rowId]['attributes']['resolution_time']); - - if(!empty($referenceId)) { - $ret[$rowId]['referenceId'] = $referenceId; - } + $ret[$rowId] = $this->filterMetadata($parsed, $referenceId); } else { - // Bump up request time, there shouldn't be a resolution_time for - // pending requests - $candidate['requestTime'] = strftime("%FT%TZ", - strtotime($parsed['request_time'])); - - unset($parsed['request_time']); - unset($parsed['resolution_time']); - - $candidate['attributes'][] = $parsed; - - $ret[] = $candidate; + // Search Reference ID + $candidate['sorRecords'][] = $this->filterMetadata($parsed, $referenceId); } } + + if($mode != 'pending') { + $ret[] = $candidate; + } } return $ret;