diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1077,6 +1077,7 @@ '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 @@ '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 @@ 'DrydockWebrootInterface' => 'DrydockInterface', 'DrydockWorker' => 'PhabricatorWorker', 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', + 'EdgeSearchConduitAPIMethod' => 'ConduitAPIMethod', 'FeedConduitAPIMethod' => 'ConduitAPIMethod', 'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod', 'FeedPublisherHTTPWorker' => 'FeedPushWorker', @@ -7652,6 +7656,11 @@ 'PhabricatorEdgeEditType' => 'PhabricatorPHIDListEditType', 'PhabricatorEdgeEditor' => 'Phobject', 'PhabricatorEdgeGraph' => 'AbstractDirectedGraph', + 'PhabricatorEdgeObject' => array( + 'Phobject', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorEdgeObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorEdgeQuery' => 'PhabricatorQuery', 'PhabricatorEdgeTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgeType' => 'Phobject', diff --git a/src/applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php b/src/applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php --- a/src/applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php +++ b/src/applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php @@ -24,4 +24,17 @@ $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.'); + } + } diff --git a/src/applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php b/src/applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php --- a/src/applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php +++ b/src/applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php @@ -13,4 +13,17 @@ 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.'); + } + } diff --git a/src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php b/src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php @@ -0,0 +1,173 @@ +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', + 'types' => 'list', + 'destinationPHIDs' => 'optional list', + ) + $this->getPagerParamTypes(); + } + + protected function defineReturnType() { + return 'list'; + } + + 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; + } +} diff --git a/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php b/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php @@ -0,0 +1,63 @@ +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; + } + +} diff --git a/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php b/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php @@ -0,0 +1,163 @@ +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], + ); + } + +} diff --git a/src/infrastructure/edges/type/PhabricatorEdgeType.php b/src/infrastructure/edges/type/PhabricatorEdgeType.php --- a/src/infrastructure/edges/type/PhabricatorEdgeType.php +++ b/src/infrastructure/edges/type/PhabricatorEdgeType.php @@ -27,6 +27,18 @@ return $const; } + public function getConduitKey() { + return null; + } + + public function getConduitName() { + return null; + } + + public function getConduitDescription() { + return null; + } + public function getInverseEdgeConstant() { return null; } diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -85,12 +85,20 @@ 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),