@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

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

+455 -1
+9
src/__phutil_library_map__.php
··· 1077 1077 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', 1078 1078 'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php', 1079 1079 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', 1080 + 'EdgeSearchConduitAPIMethod' => 'infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php', 1080 1081 'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php', 1081 1082 'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php', 1082 1083 'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php', ··· 2589 2590 'PhabricatorEdgeEditType' => 'applications/transactions/edittype/PhabricatorEdgeEditType.php', 2590 2591 'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php', 2591 2592 'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php', 2593 + 'PhabricatorEdgeObject' => 'infrastructure/edges/conduit/PhabricatorEdgeObject.php', 2594 + 'PhabricatorEdgeObjectQuery' => 'infrastructure/edges/query/PhabricatorEdgeObjectQuery.php', 2592 2595 'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php', 2593 2596 'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php', 2594 2597 'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php', ··· 5886 5889 'DrydockWebrootInterface' => 'DrydockInterface', 5887 5890 'DrydockWorker' => 'PhabricatorWorker', 5888 5891 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', 5892 + 'EdgeSearchConduitAPIMethod' => 'ConduitAPIMethod', 5889 5893 'FeedConduitAPIMethod' => 'ConduitAPIMethod', 5890 5894 'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod', 5891 5895 'FeedPublisherHTTPWorker' => 'FeedPushWorker', ··· 7652 7656 'PhabricatorEdgeEditType' => 'PhabricatorPHIDListEditType', 7653 7657 'PhabricatorEdgeEditor' => 'Phobject', 7654 7658 'PhabricatorEdgeGraph' => 'AbstractDirectedGraph', 7659 + 'PhabricatorEdgeObject' => array( 7660 + 'Phobject', 7661 + 'PhabricatorPolicyInterface', 7662 + ), 7663 + 'PhabricatorEdgeObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 7655 7664 'PhabricatorEdgeQuery' => 'PhabricatorQuery', 7656 7665 'PhabricatorEdgeTestCase' => 'PhabricatorTestCase', 7657 7666 'PhabricatorEdgeType' => 'Phobject',
+13
src/applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php
··· 24 24 $add_edges); 25 25 } 26 26 27 + public function getConduitKey() { 28 + return 'mentioned-in'; 29 + } 30 + 31 + public function getConduitName() { 32 + return pht('Mention In'); 33 + } 34 + 35 + public function getConduitDescription() { 36 + return pht( 37 + 'The source object is mentioned in a comment on the destination object.'); 38 + } 39 + 27 40 }
+13
src/applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php
··· 13 13 return true; 14 14 } 15 15 16 + public function getConduitKey() { 17 + return 'mention'; 18 + } 19 + 20 + public function getConduitName() { 21 + return pht('Mention'); 22 + } 23 + 24 + public function getConduitDescription() { 25 + return pht( 26 + 'The source object has a comment which mentions the destination object.'); 27 + } 28 + 16 29 }
+173
src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php
··· 1 + <?php 2 + 3 + final class EdgeSearchConduitAPIMethod 4 + extends ConduitAPIMethod { 5 + 6 + public function getAPIMethodName() { 7 + return 'edge.search'; 8 + } 9 + 10 + public function getMethodDescription() { 11 + return pht('Read edge relationships between objects.'); 12 + } 13 + 14 + public function getMethodDocumentation() { 15 + $viewer = $this->getViewer(); 16 + 17 + $rows = array(); 18 + foreach ($this->getConduitEdgeTypeMap() as $key => $type) { 19 + $inverse_constant = $type->getInverseEdgeConstant(); 20 + if ($inverse_constant) { 21 + $inverse_type = PhabricatorEdgeType::getByConstant($inverse_constant); 22 + $inverse = $inverse_type->getConduitKey(); 23 + } else { 24 + $inverse = null; 25 + } 26 + 27 + $rows[] = array( 28 + $key, 29 + $type->getConduitName(), 30 + $inverse, 31 + new PHUIRemarkupView($viewer, $type->getConduitDescription()), 32 + ); 33 + } 34 + 35 + $types_table = id(new AphrontTableView($rows)) 36 + ->setHeaders( 37 + array( 38 + pht('Constant'), 39 + pht('Name'), 40 + pht('Inverse'), 41 + pht('Description'), 42 + )) 43 + ->setColumnClasses( 44 + array( 45 + 'mono', 46 + 'pri', 47 + 'mono', 48 + 'wide', 49 + )); 50 + 51 + return id(new PHUIObjectBoxView()) 52 + ->setHeaderText(pht('Edge Types')) 53 + ->setTable($types_table); 54 + } 55 + 56 + public function getMethodStatus() { 57 + return self::METHOD_STATUS_UNSTABLE; 58 + } 59 + 60 + public function getMethodStatusDescription() { 61 + return pht('This method is new and experimental.'); 62 + } 63 + 64 + protected function defineParamTypes() { 65 + return array( 66 + 'sourcePHIDs' => 'list<phid>', 67 + 'types' => 'list<const>', 68 + 'destinationPHIDs' => 'optional list<phid>', 69 + ) + $this->getPagerParamTypes(); 70 + } 71 + 72 + protected function defineReturnType() { 73 + return 'list<dict>'; 74 + } 75 + 76 + protected function defineErrorTypes() { 77 + return array(); 78 + } 79 + 80 + protected function execute(ConduitAPIRequest $request) { 81 + $viewer = $request->getUser(); 82 + $pager = $this->newPager($request); 83 + 84 + $source_phids = $request->getValue('sourcePHIDs', array()); 85 + $edge_types = $request->getValue('types', array()); 86 + $destination_phids = $request->getValue('destinationPHIDs', array()); 87 + 88 + $object_query = id(new PhabricatorObjectQuery()) 89 + ->setViewer($viewer) 90 + ->withNames($source_phids); 91 + 92 + $object_query->execute(); 93 + $objects = $object_query->getNamedResults(); 94 + foreach ($source_phids as $phid) { 95 + if (empty($objects[$phid])) { 96 + throw new Exception( 97 + pht( 98 + 'Source PHID "%s" does not identify a valid object, or you do '. 99 + 'not have permission to view it.', 100 + $phid)); 101 + } 102 + } 103 + $source_phids = mpull($objects, 'getPHID'); 104 + 105 + if (!$edge_types) { 106 + throw new Exception( 107 + pht( 108 + 'Edge search must specify a nonempty list of edge types.')); 109 + } 110 + 111 + $edge_map = $this->getConduitEdgeTypeMap(); 112 + 113 + $constant_map = array(); 114 + $edge_constants = array(); 115 + foreach ($edge_types as $edge_type) { 116 + if (!isset($edge_map[$edge_type])) { 117 + throw new Exception( 118 + pht( 119 + 'Edge type "%s" is not a recognized edge type.', 120 + $edge_type)); 121 + } 122 + 123 + $constant = $edge_map[$edge_type]->getEdgeConstant(); 124 + 125 + $edge_constants[] = $constant; 126 + $constant_map[$constant] = $edge_type; 127 + } 128 + 129 + $edge_query = id(new PhabricatorEdgeObjectQuery()) 130 + ->setViewer($viewer) 131 + ->withSourcePHIDs($source_phids) 132 + ->withEdgeTypes($edge_constants); 133 + 134 + if ($destination_phids) { 135 + $edge_query->withDestinationPHIDs($destination_phids); 136 + } 137 + 138 + $edge_objects = $edge_query->executeWithCursorPager($pager); 139 + 140 + $edges = array(); 141 + foreach ($edge_objects as $edge_object) { 142 + $edges[] = array( 143 + 'sourcePHID' => $edge_object->getSourcePHID(), 144 + 'edgeType' => $constant_map[$edge_object->getEdgeType()], 145 + 'destinationPHID' => $edge_object->getDestinationPHID(), 146 + ); 147 + } 148 + 149 + $results = array( 150 + 'data' => $edges, 151 + ); 152 + 153 + return $this->addPagerResults($results, $pager); 154 + } 155 + 156 + private function getConduitEdgeTypeMap() { 157 + $types = PhabricatorEdgeType::getAllTypes(); 158 + 159 + $map = array(); 160 + foreach ($types as $type) { 161 + $key = $type->getConduitKey(); 162 + if ($key === null) { 163 + continue; 164 + } 165 + 166 + $map[$key] = $type; 167 + } 168 + 169 + ksort($map); 170 + 171 + return $map; 172 + } 173 + }
+63
src/infrastructure/edges/conduit/PhabricatorEdgeObject.php
··· 1 + <?php 2 + 3 + final class PhabricatorEdgeObject 4 + extends Phobject 5 + implements PhabricatorPolicyInterface { 6 + 7 + private $id; 8 + private $src; 9 + private $dst; 10 + private $type; 11 + 12 + public static function newFromRow(array $row) { 13 + $edge = new self(); 14 + 15 + $edge->id = $row['id']; 16 + $edge->src = $row['src']; 17 + $edge->dst = $row['dst']; 18 + $edge->type = $row['type']; 19 + 20 + return $edge; 21 + } 22 + 23 + public function getID() { 24 + return $this->id; 25 + } 26 + 27 + public function getSourcePHID() { 28 + return $this->src; 29 + } 30 + 31 + public function getEdgeType() { 32 + return $this->type; 33 + } 34 + 35 + public function getDestinationPHID() { 36 + return $this->dst; 37 + } 38 + 39 + public function getPHID() { 40 + return null; 41 + } 42 + 43 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 44 + 45 + 46 + public function getCapabilities() { 47 + return array( 48 + PhabricatorPolicyCapability::CAN_VIEW, 49 + ); 50 + } 51 + 52 + public function getPolicy($capability) { 53 + switch ($capability) { 54 + case PhabricatorPolicyCapability::CAN_VIEW: 55 + return PhabricatorPolicies::getMostOpenPolicy(); 56 + } 57 + } 58 + 59 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 60 + return false; 61 + } 62 + 63 + }
+163
src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php
··· 1 + <?php 2 + 3 + /** 4 + * This is a more formal version of @{class:PhabricatorEdgeQuery} that is used 5 + * to expose edges to Conduit. 6 + */ 7 + final class PhabricatorEdgeObjectQuery 8 + extends PhabricatorCursorPagedPolicyAwareQuery { 9 + 10 + private $sourcePHIDs; 11 + private $sourcePHIDType; 12 + private $edgeTypes; 13 + private $destinationPHIDs; 14 + 15 + 16 + public function withSourcePHIDs(array $source_phids) { 17 + $this->sourcePHIDs = $source_phids; 18 + return $this; 19 + } 20 + 21 + public function withEdgeTypes(array $types) { 22 + $this->edgeTypes = $types; 23 + return $this; 24 + } 25 + 26 + public function withDestinationPHIDs(array $destination_phids) { 27 + $this->destinationPHIDs = $destination_phids; 28 + return $this; 29 + } 30 + 31 + protected function willExecute() { 32 + $source_phids = $this->sourcePHIDs; 33 + 34 + if (!$source_phids) { 35 + throw new Exception( 36 + pht( 37 + 'Edge object query must be executed with a nonempty list of '. 38 + 'source PHIDs.')); 39 + } 40 + 41 + $phid_item = null; 42 + $phid_type = null; 43 + foreach ($source_phids as $phid) { 44 + $this_type = phid_get_type($phid); 45 + if ($this_type == PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { 46 + throw new Exception( 47 + pht( 48 + 'Source PHID "%s" in edge object query has unknown PHID type.', 49 + $phid)); 50 + } 51 + 52 + if ($phid_type === null) { 53 + $phid_type = $this_type; 54 + $phid_item = $phid; 55 + continue; 56 + } 57 + 58 + if ($phid_type !== $this_type) { 59 + throw new Exception( 60 + pht( 61 + 'Two source PHIDs ("%s" and "%s") have different PHID types '. 62 + '("%s" and "%s"). All PHIDs must be of the same type to execute '. 63 + 'an edge object query.', 64 + $phid_item, 65 + $phid, 66 + $phid_type, 67 + $this_type)); 68 + } 69 + } 70 + 71 + $this->sourcePHIDType = $phid_type; 72 + } 73 + 74 + protected function loadPage() { 75 + $type = $this->sourcePHIDType; 76 + $conn = PhabricatorEdgeConfig::establishConnection($type, 'r'); 77 + $table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; 78 + $rows = $this->loadStandardPageRowsWithConnection($conn, $table); 79 + 80 + $result = array(); 81 + foreach ($rows as $row) { 82 + $result[] = PhabricatorEdgeObject::newFromRow($row); 83 + } 84 + 85 + return $result; 86 + } 87 + 88 + protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { 89 + $parts = parent::buildSelectClauseParts($conn); 90 + 91 + // TODO: This is hacky, because we don't have real IDs on this table. 92 + $parts[] = qsprintf( 93 + $conn, 94 + 'CONCAT(dateCreated, %s, seq) AS id', 95 + '_'); 96 + 97 + return $parts; 98 + } 99 + 100 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 101 + $parts = parent::buildWhereClauseParts($conn); 102 + 103 + $parts[] = qsprintf( 104 + $conn, 105 + 'src IN (%Ls)', 106 + $this->sourcePHIDs); 107 + 108 + $parts[] = qsprintf( 109 + $conn, 110 + 'type IN (%Ls)', 111 + $this->edgeTypes); 112 + 113 + if ($this->destinationPHIDs !== null) { 114 + $parts[] = qsprintf( 115 + $conn, 116 + 'dst IN (%Ls)', 117 + $this->destinationPHIDs); 118 + } 119 + 120 + return $parts; 121 + } 122 + 123 + public function getQueryApplicationClass() { 124 + return null; 125 + } 126 + 127 + protected function getPrimaryTableAlias() { 128 + return 'edge'; 129 + } 130 + 131 + public function getOrderableColumns() { 132 + return array( 133 + 'dateCreated' => array( 134 + 'table' => 'edge', 135 + 'column' => 'dateCreated', 136 + 'type' => 'int', 137 + ), 138 + 'sequence' => array( 139 + 'table' => 'edge', 140 + 'column' => 'seq', 141 + 'type' => 'int', 142 + 143 + // TODO: This is not actually unique, but we're just doing our best 144 + // here. 145 + 'unique' => true, 146 + ), 147 + ); 148 + } 149 + 150 + protected function getDefaultOrderVector() { 151 + return array('dateCreated', 'sequence'); 152 + } 153 + 154 + protected function getPagingValueMap($cursor, array $keys) { 155 + $parts = explode('_', $cursor); 156 + 157 + return array( 158 + 'dateCreated' => $parts[0], 159 + 'sequence' => $parts[1], 160 + ); 161 + } 162 + 163 + }
+12
src/infrastructure/edges/type/PhabricatorEdgeType.php
··· 27 27 return $const; 28 28 } 29 29 30 + public function getConduitKey() { 31 + return null; 32 + } 33 + 34 + public function getConduitName() { 35 + return null; 36 + } 37 + 38 + public function getConduitDescription() { 39 + return null; 40 + } 41 + 30 42 public function getInverseEdgeConstant() { 31 43 return null; 32 44 }
+9 -1
src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
··· 85 85 86 86 protected function loadStandardPageRows(PhabricatorLiskDAO $table) { 87 87 $conn = $table->establishConnection('r'); 88 + return $this->loadStandardPageRowsWithConnection( 89 + $conn, 90 + $table->getTableName()); 91 + } 92 + 93 + protected function loadStandardPageRowsWithConnection( 94 + AphrontDatabaseConnection $conn, 95 + $table_name) { 88 96 89 97 $rows = queryfx_all( 90 98 $conn, 91 99 '%Q FROM %T %Q %Q %Q %Q %Q %Q %Q', 92 100 $this->buildSelectClause($conn), 93 - $table->getTableName(), 101 + $table_name, 94 102 (string)$this->getPrimaryTableAlias(), 95 103 $this->buildJoinClause($conn), 96 104 $this->buildWhereClause($conn),