mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-23 14:00:56 +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:
parent
9ccef52d6c
commit
be16f9b2cd
8 changed files with 455 additions and 1 deletions
|
@ -1077,6 +1077,7 @@ phutil_register_library_map(array(
|
|||
'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php',
|
||||
'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php',
|
||||
'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php',
|
||||
'EdgeSearchConduitAPIMethod' => 'infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php',
|
||||
'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php',
|
||||
'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php',
|
||||
'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php',
|
||||
|
@ -2589,6 +2590,8 @@ phutil_register_library_map(array(
|
|||
'PhabricatorEdgeEditType' => 'applications/transactions/edittype/PhabricatorEdgeEditType.php',
|
||||
'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.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',
|
||||
'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php',
|
||||
'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php',
|
||||
|
@ -5886,6 +5889,7 @@ phutil_register_library_map(array(
|
|||
'DrydockWebrootInterface' => 'DrydockInterface',
|
||||
'DrydockWorker' => 'PhabricatorWorker',
|
||||
'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation',
|
||||
'EdgeSearchConduitAPIMethod' => 'ConduitAPIMethod',
|
||||
'FeedConduitAPIMethod' => 'ConduitAPIMethod',
|
||||
'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod',
|
||||
'FeedPublisherHTTPWorker' => 'FeedPushWorker',
|
||||
|
@ -7652,6 +7656,11 @@ phutil_register_library_map(array(
|
|||
'PhabricatorEdgeEditType' => 'PhabricatorPHIDListEditType',
|
||||
'PhabricatorEdgeEditor' => 'Phobject',
|
||||
'PhabricatorEdgeGraph' => 'AbstractDirectedGraph',
|
||||
'PhabricatorEdgeObject' => array(
|
||||
'Phobject',
|
||||
'PhabricatorPolicyInterface',
|
||||
),
|
||||
'PhabricatorEdgeObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorEdgeQuery' => 'PhabricatorQuery',
|
||||
'PhabricatorEdgeTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorEdgeType' => 'Phobject',
|
||||
|
|
|
@ -24,4 +24,17 @@ final class PhabricatorObjectMentionedByObjectEdgeType
|
|||
$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.');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,4 +13,17 @@ final class PhabricatorObjectMentionsObjectEdgeType
|
|||
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.');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
173
src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php
Normal file
173
src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php
Normal 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;
|
||||
}
|
||||
}
|
63
src/infrastructure/edges/conduit/PhabricatorEdgeObject.php
Normal file
63
src/infrastructure/edges/conduit/PhabricatorEdgeObject.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
163
src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php
Normal file
163
src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php
Normal 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],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -27,6 +27,18 @@ abstract class PhabricatorEdgeType extends Phobject {
|
|||
return $const;
|
||||
}
|
||||
|
||||
public function getConduitKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getConduitName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getConduitDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getInverseEdgeConstant() {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -85,12 +85,20 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
|
||||
protected function loadStandardPageRows(PhabricatorLiskDAO $table) {
|
||||
$conn = $table->establishConnection('r');
|
||||
return $this->loadStandardPageRowsWithConnection(
|
||||
$conn,
|
||||
$table->getTableName());
|
||||
}
|
||||
|
||||
protected function loadStandardPageRowsWithConnection(
|
||||
AphrontDatabaseConnection $conn,
|
||||
$table_name) {
|
||||
|
||||
$rows = queryfx_all(
|
||||
$conn,
|
||||
'%Q FROM %T %Q %Q %Q %Q %Q %Q %Q',
|
||||
$this->buildSelectClause($conn),
|
||||
$table->getTableName(),
|
||||
$table_name,
|
||||
(string)$this->getPrimaryTableAlias(),
|
||||
$this->buildJoinClause($conn),
|
||||
$this->buildWhereClause($conn),
|
||||
|
|
Loading…
Reference in a new issue