1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-23 07:12:41 +01:00

Add a generic "edge.search" method

Summary:
Ref T12337. Ref T5873. This provides a generic "edge.search" method which feels like other "verison 3" `*.search` methods.

The major issues here are:

  1. Edges use constants internally, which aren't great for an API.
  2. A lot of edges are internal and probably not useful to query.
  3. Edges don't have a real "id", so paginating them properly is challenging.

I've solved these things like this:

  - Edges must opt-in to being available via Conduit by providing a human-readable key (like "mention" instead of "52"). This solvs (1) and (2).
  - I faked a mostly-reasonable behavior for paginating.

Test Plan:
Ran various valid and invalid searches. Paginated a large search. Reviewed UI.

{F3651818}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T12337, T5873

Differential Revision: https://secure.phabricator.com/D17462
This commit is contained in:
epriestley 2017-03-04 08:49:52 -08:00
parent 9ccef52d6c
commit be16f9b2cd
8 changed files with 455 additions and 1 deletions

View file

@ -1077,6 +1077,7 @@ phutil_register_library_map(array(
'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php',
'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php', 'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php',
'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php',
'EdgeSearchConduitAPIMethod' => 'infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php',
'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php', 'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php',
'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php', 'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php',
'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php', 'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php',
@ -2589,6 +2590,8 @@ phutil_register_library_map(array(
'PhabricatorEdgeEditType' => 'applications/transactions/edittype/PhabricatorEdgeEditType.php', 'PhabricatorEdgeEditType' => 'applications/transactions/edittype/PhabricatorEdgeEditType.php',
'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php', 'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php',
'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php', 'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php',
'PhabricatorEdgeObject' => 'infrastructure/edges/conduit/PhabricatorEdgeObject.php',
'PhabricatorEdgeObjectQuery' => 'infrastructure/edges/query/PhabricatorEdgeObjectQuery.php',
'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php', 'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php',
'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php', 'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php',
'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php', 'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php',
@ -5886,6 +5889,7 @@ phutil_register_library_map(array(
'DrydockWebrootInterface' => 'DrydockInterface', 'DrydockWebrootInterface' => 'DrydockInterface',
'DrydockWorker' => 'PhabricatorWorker', 'DrydockWorker' => 'PhabricatorWorker',
'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation',
'EdgeSearchConduitAPIMethod' => 'ConduitAPIMethod',
'FeedConduitAPIMethod' => 'ConduitAPIMethod', 'FeedConduitAPIMethod' => 'ConduitAPIMethod',
'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod', 'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod',
'FeedPublisherHTTPWorker' => 'FeedPushWorker', 'FeedPublisherHTTPWorker' => 'FeedPushWorker',
@ -7652,6 +7656,11 @@ phutil_register_library_map(array(
'PhabricatorEdgeEditType' => 'PhabricatorPHIDListEditType', 'PhabricatorEdgeEditType' => 'PhabricatorPHIDListEditType',
'PhabricatorEdgeEditor' => 'Phobject', 'PhabricatorEdgeEditor' => 'Phobject',
'PhabricatorEdgeGraph' => 'AbstractDirectedGraph', 'PhabricatorEdgeGraph' => 'AbstractDirectedGraph',
'PhabricatorEdgeObject' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorEdgeObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEdgeQuery' => 'PhabricatorQuery', 'PhabricatorEdgeQuery' => 'PhabricatorQuery',
'PhabricatorEdgeTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgeTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgeType' => 'Phobject', 'PhabricatorEdgeType' => 'Phobject',

View file

@ -24,4 +24,17 @@ final class PhabricatorObjectMentionedByObjectEdgeType
$add_edges); $add_edges);
} }
public function getConduitKey() {
return 'mentioned-in';
}
public function getConduitName() {
return pht('Mention In');
}
public function getConduitDescription() {
return pht(
'The source object is mentioned in a comment on the destination object.');
}
} }

View file

@ -13,4 +13,17 @@ final class PhabricatorObjectMentionsObjectEdgeType
return true; return true;
} }
public function getConduitKey() {
return 'mention';
}
public function getConduitName() {
return pht('Mention');
}
public function getConduitDescription() {
return pht(
'The source object has a comment which mentions the destination object.');
}
} }

View file

@ -0,0 +1,173 @@
<?php
final class EdgeSearchConduitAPIMethod
extends ConduitAPIMethod {
public function getAPIMethodName() {
return 'edge.search';
}
public function getMethodDescription() {
return pht('Read edge relationships between objects.');
}
public function getMethodDocumentation() {
$viewer = $this->getViewer();
$rows = array();
foreach ($this->getConduitEdgeTypeMap() as $key => $type) {
$inverse_constant = $type->getInverseEdgeConstant();
if ($inverse_constant) {
$inverse_type = PhabricatorEdgeType::getByConstant($inverse_constant);
$inverse = $inverse_type->getConduitKey();
} else {
$inverse = null;
}
$rows[] = array(
$key,
$type->getConduitName(),
$inverse,
new PHUIRemarkupView($viewer, $type->getConduitDescription()),
);
}
$types_table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Constant'),
pht('Name'),
pht('Inverse'),
pht('Description'),
))
->setColumnClasses(
array(
'mono',
'pri',
'mono',
'wide',
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Edge Types'))
->setTable($types_table);
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
return pht('This method is new and experimental.');
}
protected function defineParamTypes() {
return array(
'sourcePHIDs' => 'list<phid>',
'types' => 'list<const>',
'destinationPHIDs' => 'optional list<phid>',
) + $this->getPagerParamTypes();
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$pager = $this->newPager($request);
$source_phids = $request->getValue('sourcePHIDs', array());
$edge_types = $request->getValue('types', array());
$destination_phids = $request->getValue('destinationPHIDs', array());
$object_query = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames($source_phids);
$object_query->execute();
$objects = $object_query->getNamedResults();
foreach ($source_phids as $phid) {
if (empty($objects[$phid])) {
throw new Exception(
pht(
'Source PHID "%s" does not identify a valid object, or you do '.
'not have permission to view it.',
$phid));
}
}
$source_phids = mpull($objects, 'getPHID');
if (!$edge_types) {
throw new Exception(
pht(
'Edge search must specify a nonempty list of edge types.'));
}
$edge_map = $this->getConduitEdgeTypeMap();
$constant_map = array();
$edge_constants = array();
foreach ($edge_types as $edge_type) {
if (!isset($edge_map[$edge_type])) {
throw new Exception(
pht(
'Edge type "%s" is not a recognized edge type.',
$edge_type));
}
$constant = $edge_map[$edge_type]->getEdgeConstant();
$edge_constants[] = $constant;
$constant_map[$constant] = $edge_type;
}
$edge_query = id(new PhabricatorEdgeObjectQuery())
->setViewer($viewer)
->withSourcePHIDs($source_phids)
->withEdgeTypes($edge_constants);
if ($destination_phids) {
$edge_query->withDestinationPHIDs($destination_phids);
}
$edge_objects = $edge_query->executeWithCursorPager($pager);
$edges = array();
foreach ($edge_objects as $edge_object) {
$edges[] = array(
'sourcePHID' => $edge_object->getSourcePHID(),
'edgeType' => $constant_map[$edge_object->getEdgeType()],
'destinationPHID' => $edge_object->getDestinationPHID(),
);
}
$results = array(
'data' => $edges,
);
return $this->addPagerResults($results, $pager);
}
private function getConduitEdgeTypeMap() {
$types = PhabricatorEdgeType::getAllTypes();
$map = array();
foreach ($types as $type) {
$key = $type->getConduitKey();
if ($key === null) {
continue;
}
$map[$key] = $type;
}
ksort($map);
return $map;
}
}

View file

@ -0,0 +1,63 @@
<?php
final class PhabricatorEdgeObject
extends Phobject
implements PhabricatorPolicyInterface {
private $id;
private $src;
private $dst;
private $type;
public static function newFromRow(array $row) {
$edge = new self();
$edge->id = $row['id'];
$edge->src = $row['src'];
$edge->dst = $row['dst'];
$edge->type = $row['type'];
return $edge;
}
public function getID() {
return $this->id;
}
public function getSourcePHID() {
return $this->src;
}
public function getEdgeType() {
return $this->type;
}
public function getDestinationPHID() {
return $this->dst;
}
public function getPHID() {
return null;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}

View file

@ -0,0 +1,163 @@
<?php
/**
* This is a more formal version of @{class:PhabricatorEdgeQuery} that is used
* to expose edges to Conduit.
*/
final class PhabricatorEdgeObjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $sourcePHIDs;
private $sourcePHIDType;
private $edgeTypes;
private $destinationPHIDs;
public function withSourcePHIDs(array $source_phids) {
$this->sourcePHIDs = $source_phids;
return $this;
}
public function withEdgeTypes(array $types) {
$this->edgeTypes = $types;
return $this;
}
public function withDestinationPHIDs(array $destination_phids) {
$this->destinationPHIDs = $destination_phids;
return $this;
}
protected function willExecute() {
$source_phids = $this->sourcePHIDs;
if (!$source_phids) {
throw new Exception(
pht(
'Edge object query must be executed with a nonempty list of '.
'source PHIDs.'));
}
$phid_item = null;
$phid_type = null;
foreach ($source_phids as $phid) {
$this_type = phid_get_type($phid);
if ($this_type == PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
throw new Exception(
pht(
'Source PHID "%s" in edge object query has unknown PHID type.',
$phid));
}
if ($phid_type === null) {
$phid_type = $this_type;
$phid_item = $phid;
continue;
}
if ($phid_type !== $this_type) {
throw new Exception(
pht(
'Two source PHIDs ("%s" and "%s") have different PHID types '.
'("%s" and "%s"). All PHIDs must be of the same type to execute '.
'an edge object query.',
$phid_item,
$phid,
$phid_type,
$this_type));
}
}
$this->sourcePHIDType = $phid_type;
}
protected function loadPage() {
$type = $this->sourcePHIDType;
$conn = PhabricatorEdgeConfig::establishConnection($type, 'r');
$table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$rows = $this->loadStandardPageRowsWithConnection($conn, $table);
$result = array();
foreach ($rows as $row) {
$result[] = PhabricatorEdgeObject::newFromRow($row);
}
return $result;
}
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$parts = parent::buildSelectClauseParts($conn);
// TODO: This is hacky, because we don't have real IDs on this table.
$parts[] = qsprintf(
$conn,
'CONCAT(dateCreated, %s, seq) AS id',
'_');
return $parts;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$parts = parent::buildWhereClauseParts($conn);
$parts[] = qsprintf(
$conn,
'src IN (%Ls)',
$this->sourcePHIDs);
$parts[] = qsprintf(
$conn,
'type IN (%Ls)',
$this->edgeTypes);
if ($this->destinationPHIDs !== null) {
$parts[] = qsprintf(
$conn,
'dst IN (%Ls)',
$this->destinationPHIDs);
}
return $parts;
}
public function getQueryApplicationClass() {
return null;
}
protected function getPrimaryTableAlias() {
return 'edge';
}
public function getOrderableColumns() {
return array(
'dateCreated' => array(
'table' => 'edge',
'column' => 'dateCreated',
'type' => 'int',
),
'sequence' => array(
'table' => 'edge',
'column' => 'seq',
'type' => 'int',
// TODO: This is not actually unique, but we're just doing our best
// here.
'unique' => true,
),
);
}
protected function getDefaultOrderVector() {
return array('dateCreated', 'sequence');
}
protected function getPagingValueMap($cursor, array $keys) {
$parts = explode('_', $cursor);
return array(
'dateCreated' => $parts[0],
'sequence' => $parts[1],
);
}
}

View file

@ -27,6 +27,18 @@ abstract class PhabricatorEdgeType extends Phobject {
return $const; return $const;
} }
public function getConduitKey() {
return null;
}
public function getConduitName() {
return null;
}
public function getConduitDescription() {
return null;
}
public function getInverseEdgeConstant() { public function getInverseEdgeConstant() {
return null; return null;
} }

View file

@ -85,12 +85,20 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
protected function loadStandardPageRows(PhabricatorLiskDAO $table) { protected function loadStandardPageRows(PhabricatorLiskDAO $table) {
$conn = $table->establishConnection('r'); $conn = $table->establishConnection('r');
return $this->loadStandardPageRowsWithConnection(
$conn,
$table->getTableName());
}
protected function loadStandardPageRowsWithConnection(
AphrontDatabaseConnection $conn,
$table_name) {
$rows = queryfx_all( $rows = queryfx_all(
$conn, $conn,
'%Q FROM %T %Q %Q %Q %Q %Q %Q %Q', '%Q FROM %T %Q %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn), $this->buildSelectClause($conn),
$table->getTableName(), $table_name,
(string)$this->getPrimaryTableAlias(), (string)$this->getPrimaryTableAlias(),
$this->buildJoinClause($conn), $this->buildJoinClause($conn),
$this->buildWhereClause($conn), $this->buildWhereClause($conn),