mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-23 15:22:41 +01:00
Implement a rough initial version of ApplicationSearch-driven Conduit read endpoints
Summary: Ref T9964. See that task for some context and discussion. Ref T7715, which has the bigger picture here. Basically, I want Conduit read endpoints to be full-power, ApplicationSearch-driven endpoints, so that applications can: - Write one EditEngine and get web + conduit writes for free. - Write one SearchEngine and get web + conduit reads for free. I previously made some steps toward this, but this puts more of the structure in place. Test Plan: Viewed API console endpoint and read 20 pages of docs: {F1021961} Made various calls: with query keys, constraints, pagination, and limits. Viewed new {nav Config > Modules} page. Reviewers: chad Reviewed By: chad Maniphest Tasks: T7715, T9964 Differential Revision: https://secure.phabricator.com/D14743
This commit is contained in:
parent
ab7d3caa00
commit
4ec6990ca7
13 changed files with 950 additions and 1 deletions
|
@ -236,6 +236,7 @@ phutil_register_library_map(array(
|
|||
'ConduitMethodNotFoundException' => 'applications/conduit/protocol/exception/ConduitMethodNotFoundException.php',
|
||||
'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php',
|
||||
'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php',
|
||||
'ConduitResultSearchEngineExtension' => 'applications/conduit/query/ConduitResultSearchEngineExtension.php',
|
||||
'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php',
|
||||
'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php',
|
||||
'ConpherenceColumnViewController' => 'applications/conpherence/controller/ConpherenceColumnViewController.php',
|
||||
|
@ -1557,6 +1558,7 @@ phutil_register_library_map(array(
|
|||
'PasteMailReceiver' => 'applications/paste/mail/PasteMailReceiver.php',
|
||||
'PasteQueryConduitAPIMethod' => 'applications/paste/conduit/PasteQueryConduitAPIMethod.php',
|
||||
'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php',
|
||||
'PasteSearchConduitAPIMethod' => 'applications/paste/conduit/PasteSearchConduitAPIMethod.php',
|
||||
'PeopleBrowseUserDirectoryCapability' => 'applications/people/capability/PeopleBrowseUserDirectoryCapability.php',
|
||||
'PeopleCreateUsersCapability' => 'applications/people/capability/PeopleCreateUsersCapability.php',
|
||||
'PeopleUserLogGarbageCollector' => 'applications/people/garbagecollector/PeopleUserLogGarbageCollector.php',
|
||||
|
@ -1880,6 +1882,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
|
||||
'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
|
||||
'PhabricatorConduitRequestExceptionHandler' => 'aphront/handler/PhabricatorConduitRequestExceptionHandler.php',
|
||||
'PhabricatorConduitResultInterface' => 'applications/conduit/interface/PhabricatorConduitResultInterface.php',
|
||||
'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php',
|
||||
'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php',
|
||||
'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php',
|
||||
|
@ -2364,6 +2367,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php',
|
||||
'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php',
|
||||
'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php',
|
||||
'PhabricatorLiskSearchEngineExtension' => 'applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php',
|
||||
'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php',
|
||||
'PhabricatorListFilterUIExample' => 'applications/uiexample/examples/PhabricatorListFilterUIExample.php',
|
||||
'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php',
|
||||
|
@ -2745,6 +2749,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php',
|
||||
'PhabricatorPolicyRequestExceptionHandler' => 'aphront/handler/PhabricatorPolicyRequestExceptionHandler.php',
|
||||
'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php',
|
||||
'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php',
|
||||
'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php',
|
||||
'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
|
||||
'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php',
|
||||
|
@ -2980,6 +2985,9 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSearchDocumentTypeDatasource' => 'applications/search/typeahead/PhabricatorSearchDocumentTypeDatasource.php',
|
||||
'PhabricatorSearchEditController' => 'applications/search/controller/PhabricatorSearchEditController.php',
|
||||
'PhabricatorSearchEngine' => 'applications/search/engine/PhabricatorSearchEngine.php',
|
||||
'PhabricatorSearchEngineAPIMethod' => 'applications/search/engine/PhabricatorSearchEngineAPIMethod.php',
|
||||
'PhabricatorSearchEngineExtension' => 'applications/search/engineextension/PhabricatorSearchEngineExtension.php',
|
||||
'PhabricatorSearchEngineExtensionModule' => 'applications/search/engineextension/PhabricatorSearchEngineExtensionModule.php',
|
||||
'PhabricatorSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php',
|
||||
'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php',
|
||||
'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php',
|
||||
|
@ -3067,6 +3075,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSpacesNoAccessController' => 'applications/spaces/controller/PhabricatorSpacesNoAccessController.php',
|
||||
'PhabricatorSpacesRemarkupRule' => 'applications/spaces/remarkup/PhabricatorSpacesRemarkupRule.php',
|
||||
'PhabricatorSpacesSchemaSpec' => 'applications/spaces/storage/PhabricatorSpacesSchemaSpec.php',
|
||||
'PhabricatorSpacesSearchEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesSearchEngineExtension.php',
|
||||
'PhabricatorSpacesSearchField' => 'applications/spaces/searchfield/PhabricatorSpacesSearchField.php',
|
||||
'PhabricatorSpacesTestCase' => 'applications/spaces/__tests__/PhabricatorSpacesTestCase.php',
|
||||
'PhabricatorSpacesViewController' => 'applications/spaces/controller/PhabricatorSpacesViewController.php',
|
||||
|
@ -4063,6 +4072,7 @@ phutil_register_library_map(array(
|
|||
'ConduitMethodNotFoundException' => 'ConduitException',
|
||||
'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod',
|
||||
'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod',
|
||||
'ConduitResultSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
|
||||
'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow',
|
||||
'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector',
|
||||
'ConpherenceColumnViewController' => 'ConpherenceController',
|
||||
|
@ -5577,6 +5587,7 @@ phutil_register_library_map(array(
|
|||
'PasteMailReceiver' => 'PhabricatorObjectMailReceiver',
|
||||
'PasteQueryConduitAPIMethod' => 'PasteConduitAPIMethod',
|
||||
'PasteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
|
||||
'PasteSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
|
||||
'PeopleBrowseUserDirectoryCapability' => 'PhabricatorPolicyCapability',
|
||||
'PeopleCreateUsersCapability' => 'PhabricatorPolicyCapability',
|
||||
'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector',
|
||||
|
@ -5962,6 +5973,7 @@ phutil_register_library_map(array(
|
|||
),
|
||||
'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorConduitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
|
||||
'PhabricatorConduitResultInterface' => 'PhabricatorPHIDInterface',
|
||||
'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||
'PhabricatorConduitTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorConduitToken' => array(
|
||||
|
@ -6527,6 +6539,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||
'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist',
|
||||
'PhabricatorLiskDAO' => 'LiskDAO',
|
||||
'PhabricatorLiskSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
|
||||
'PhabricatorLiskSerializer' => 'Phobject',
|
||||
'PhabricatorListFilterUIExample' => 'PhabricatorUIExample',
|
||||
'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine',
|
||||
|
@ -6823,6 +6836,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorDestructibleInterface',
|
||||
'PhabricatorApplicationTransactionInterface',
|
||||
'PhabricatorSpacesInterface',
|
||||
'PhabricatorConduitResultInterface',
|
||||
),
|
||||
'PhabricatorPasteApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorPasteArchiveController' => 'PhabricatorPasteController',
|
||||
|
@ -6965,6 +6979,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorPolicyRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
|
||||
'PhabricatorPolicyRule' => 'Phobject',
|
||||
'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension',
|
||||
'PhabricatorPolicyTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorPolicyTestObject' => array(
|
||||
'Phobject',
|
||||
|
@ -7265,6 +7280,9 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSearchDocumentTypeDatasource' => 'PhabricatorTypeaheadDatasource',
|
||||
'PhabricatorSearchEditController' => 'PhabricatorSearchBaseController',
|
||||
'PhabricatorSearchEngine' => 'Phobject',
|
||||
'PhabricatorSearchEngineAPIMethod' => 'ConduitAPIMethod',
|
||||
'PhabricatorSearchEngineExtension' => 'Phobject',
|
||||
'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule',
|
||||
'PhabricatorSearchEngineTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorSearchField' => 'Phobject',
|
||||
'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController',
|
||||
|
@ -7367,6 +7385,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSpacesNoAccessController' => 'PhabricatorSpacesController',
|
||||
'PhabricatorSpacesRemarkupRule' => 'PhabricatorObjectRemarkupRule',
|
||||
'PhabricatorSpacesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
|
||||
'PhabricatorSpacesSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
|
||||
'PhabricatorSpacesSearchField' => 'PhabricatorSearchTokenizerField',
|
||||
'PhabricatorSpacesTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorSpacesViewController' => 'PhabricatorSpacesController',
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
interface PhabricatorConduitResultInterface
|
||||
extends PhabricatorPHIDInterface {
|
||||
|
||||
public function getFieldSpecificationsForConduit();
|
||||
public function getFieldValuesForConduit();
|
||||
|
||||
}
|
||||
|
||||
// TEMPLATE IMPLEMENTATION /////////////////////////////////////////////////////
|
||||
|
||||
/* -( PhabricatorConduitResultInterface )---------------------------------- */
|
||||
/*
|
||||
|
||||
public function getFieldSpecificationsForConduit() {
|
||||
return array(
|
||||
'name' => array(
|
||||
'type' => 'string',
|
||||
'description' => pht('The name of the object.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getFieldValuesForConduit() {
|
||||
return array(
|
||||
'name' => $this->getName(),
|
||||
);
|
||||
}
|
||||
|
||||
*/
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
final class ConduitResultSearchEngineExtension
|
||||
extends PhabricatorSearchEngineExtension {
|
||||
|
||||
const EXTENSIONKEY = 'conduit';
|
||||
|
||||
public function isExtensionEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getExtensionName() {
|
||||
return pht('Support for ConduitResultInterface');
|
||||
}
|
||||
|
||||
public function supportsObject($object) {
|
||||
return ($object instanceof PhabricatorConduitResultInterface);
|
||||
}
|
||||
|
||||
public function getFieldSpecificationsForConduit($object) {
|
||||
return $object->getFieldSpecificationsForConduit();
|
||||
}
|
||||
|
||||
public function getFieldValuesForConduit($object) {
|
||||
return $object->getFieldValuesForConduit();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
final class PasteSearchConduitAPIMethod
|
||||
extends PhabricatorSearchEngineAPIMethod {
|
||||
|
||||
public function getAPIMethodName() {
|
||||
return 'paste.search';
|
||||
}
|
||||
|
||||
public function newSearchEngine() {
|
||||
return new PhabricatorPasteSearchEngine();
|
||||
}
|
||||
|
||||
public function getMethodSummary() {
|
||||
return pht('Read information about pastes.');
|
||||
}
|
||||
|
||||
}
|
|
@ -10,7 +10,8 @@ final class PhabricatorPaste extends PhabricatorPasteDAO
|
|||
PhabricatorProjectInterface,
|
||||
PhabricatorDestructibleInterface,
|
||||
PhabricatorApplicationTransactionInterface,
|
||||
PhabricatorSpacesInterface {
|
||||
PhabricatorSpacesInterface,
|
||||
PhabricatorConduitResultInterface {
|
||||
|
||||
protected $title;
|
||||
protected $authorPHID;
|
||||
|
@ -250,4 +251,38 @@ final class PhabricatorPaste extends PhabricatorPasteDAO
|
|||
return $this->spacePHID;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorConduitResultInterface )---------------------------------- */
|
||||
|
||||
|
||||
public function getFieldSpecificationsForConduit() {
|
||||
return array(
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
'description' => pht('The name of the object.'),
|
||||
),
|
||||
'authorPHID' => array(
|
||||
'type' => 'phid',
|
||||
'description' => pht('User PHID of the author.'),
|
||||
),
|
||||
'language' => array(
|
||||
'type' => 'string?',
|
||||
'description' => pht('Language to use for syntax highlighting.'),
|
||||
),
|
||||
'status' => array(
|
||||
'type' => 'string',
|
||||
'description' => pht('Active or archived status of the paste.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getFieldValuesForConduit() {
|
||||
return array(
|
||||
'title' => $this->getTitle(),
|
||||
'authorPHID' => $this->getAuthorPHID(),
|
||||
'language' => nonempty($this->getLanguage(), null),
|
||||
'status' => $this->getStatus(),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPolicySearchEngineExtension
|
||||
extends PhabricatorSearchEngineExtension {
|
||||
|
||||
const EXTENSIONKEY = 'policy';
|
||||
|
||||
public function isExtensionEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getExtensionName() {
|
||||
return pht('Support for Policies');
|
||||
}
|
||||
|
||||
public function supportsObject($object) {
|
||||
return ($object instanceof PhabricatorPolicyInterface);
|
||||
}
|
||||
|
||||
public function getFieldSpecificationsForConduit($object) {
|
||||
return array(
|
||||
'policy' => array(
|
||||
'type' => 'map<string, wild>',
|
||||
'description' => pht(
|
||||
'Map of capabilities to current policies.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getFieldValuesForConduit($object) {
|
||||
$capabilities = $object->getCapabilities();
|
||||
|
||||
$map = array();
|
||||
foreach ($capabilities as $capability) {
|
||||
$map[$capability] = $object->getPolicy($capability);
|
||||
}
|
||||
|
||||
return array(
|
||||
'policy' => $map,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1172,4 +1172,197 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
|
|||
return $fields;
|
||||
}
|
||||
|
||||
public function getSearchFieldsForConduit() {
|
||||
$fields = $this->buildSearchFields();
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function buildConduitResponse(ConduitAPIRequest $request) {
|
||||
$viewer = $this->requireViewer();
|
||||
$fields = $this->buildSearchFields();
|
||||
|
||||
$query_key = $request->getValue('queryKey');
|
||||
if (!strlen($query_key)) {
|
||||
$saved_query = new PhabricatorSavedQuery();
|
||||
} else if ($this->isBuiltinQuery($query_key)) {
|
||||
$saved_query = $this->buildSavedQueryFromBuiltin($query_key);
|
||||
} else {
|
||||
$saved_query = id(new PhabricatorSavedQueryQuery())
|
||||
->setViewer($viewer)
|
||||
->withQueryKeys(array($query_key))
|
||||
->executeOne();
|
||||
if (!$saved_query) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Query key "%s" does not correspond to a valid query.',
|
||||
$query_key));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$field->setViewer($viewer);
|
||||
}
|
||||
|
||||
$constraints = $request->getValue('constraints', array());
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (!$field->getValueExistsInConduitRequest($constraints)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $field->readValueFromConduitRequest($constraints);
|
||||
$saved_query->setParameter($field->getKey(), $value);
|
||||
}
|
||||
|
||||
$this->saveQuery($saved_query);
|
||||
|
||||
|
||||
$query = $this->buildQueryFromSavedQuery($saved_query);
|
||||
$pager = $this->newPagerForSavedQuery($saved_query);
|
||||
|
||||
$this->setQueryOrderForConduit($query, $request);
|
||||
$this->setPagerLimitForConduit($pager, $request);
|
||||
$this->setPagerOffsetsForConduit($pager, $request);
|
||||
|
||||
$objects = $this->executeQuery($query, $pager);
|
||||
|
||||
$data = array();
|
||||
if ($objects) {
|
||||
$field_extensions = $this->getConduitFieldExtensions();
|
||||
|
||||
foreach ($objects as $object) {
|
||||
$data[] = $this->getObjectWireFormatForConduit(
|
||||
$object,
|
||||
$field_extensions);
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'data' => $data,
|
||||
'query' => array(
|
||||
'queryKey' => $saved_query->getQueryKey(),
|
||||
),
|
||||
'cursor' => array(
|
||||
'limit' => $pager->getPageSize(),
|
||||
'after' => $pager->getNextPageID(),
|
||||
'before' => $pager->getPrevPageID(),
|
||||
'order' => $request->getValue('order'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getAllConduitFieldSpecifications() {
|
||||
$extensions = $this->getConduitFieldExtensions();
|
||||
$object = $this->newQuery()->newResultObject();
|
||||
|
||||
$specifications = array();
|
||||
foreach ($extensions as $extension) {
|
||||
$specifications += $extension->getFieldSpecificationsForConduit($object);
|
||||
}
|
||||
|
||||
return $specifications;
|
||||
}
|
||||
|
||||
private function getConduitFieldExtensions() {
|
||||
$extensions = PhabricatorSearchEngineExtension::getAllEnabledExtensions();
|
||||
$object = $this->newQuery()->newResultObject();
|
||||
|
||||
$field_extensions = array();
|
||||
foreach ($extensions as $key => $extension) {
|
||||
if ($extension->getFieldSpecificationsForConduit($object)) {
|
||||
$field_extensions[$key] = $extension;
|
||||
}
|
||||
}
|
||||
|
||||
return $field_extensions;
|
||||
}
|
||||
|
||||
private function setQueryOrderForConduit($query, ConduitAPIRequest $request) {
|
||||
$order = $request->getValue('order');
|
||||
if ($order === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_scalar($order)) {
|
||||
$query->setOrder($order);
|
||||
} else {
|
||||
$query->setOrderVector($order);
|
||||
}
|
||||
}
|
||||
|
||||
private function setPagerLimitForConduit($pager, ConduitAPIRequest $request) {
|
||||
$limit = $request->getValue('limit');
|
||||
|
||||
// If there's no limit specified and the query uses a weird huge page
|
||||
// size, just leave it at the default gigantic page size. Otherwise,
|
||||
// make sure it's between 1 and 100, inclusive.
|
||||
|
||||
if ($limit === null) {
|
||||
if ($pager->getPageSize() >= 0xFFFF) {
|
||||
return;
|
||||
} else {
|
||||
$limit = 100;
|
||||
}
|
||||
}
|
||||
|
||||
if ($limit > 100) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Maximum page size for Conduit API method calls is 100, but '.
|
||||
'this call specified %s.',
|
||||
$limit));
|
||||
}
|
||||
|
||||
if ($limit < 1) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Minimum page size for API searches is 1, but this call '.
|
||||
'specified %s.',
|
||||
$limit));
|
||||
}
|
||||
|
||||
$pager->setPageSize($limit);
|
||||
}
|
||||
|
||||
private function setPagerOffsetsForConduit(
|
||||
$pager,
|
||||
ConduitAPIRequest $request) {
|
||||
$before_id = $request->getValue('before');
|
||||
if ($before_id !== null) {
|
||||
$pager->setBeforeID($before_id);
|
||||
}
|
||||
|
||||
$after_id = $request->getValue('after');
|
||||
if ($after_id !== null) {
|
||||
$pager->setAfterID($after_id);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getObjectWireFormatForConduit(
|
||||
$object,
|
||||
array $field_extensions) {
|
||||
$phid = $object->getPHID();
|
||||
|
||||
return array(
|
||||
'id' => (int)$object->getID(),
|
||||
'type' => phid_get_type($phid),
|
||||
'phid' => $phid,
|
||||
'fields' => $this->getObjectWireFieldsForConduit(
|
||||
$object,
|
||||
$field_extensions),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getObjectWireFieldsForConduit(
|
||||
$object,
|
||||
array $field_extensions) {
|
||||
|
||||
$fields = array();
|
||||
foreach ($field_extensions as $extension) {
|
||||
$fields += $extension->getFieldValuesForConduit($object);
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorSearchEngineAPIMethod
|
||||
extends ConduitAPIMethod {
|
||||
|
||||
abstract public function newSearchEngine();
|
||||
|
||||
public function getApplication() {
|
||||
$engine = $this->newSearchEngine();
|
||||
$class = $engine->getApplicationClassName();
|
||||
return PhabricatorApplication::getByClass($class);
|
||||
}
|
||||
|
||||
public function getMethodStatus() {
|
||||
return self::METHOD_STATUS_UNSTABLE;
|
||||
}
|
||||
|
||||
public function getMethodStatusDescription() {
|
||||
return pht('ApplicationSearch methods are highly unstable.');
|
||||
}
|
||||
|
||||
final protected function defineParamTypes() {
|
||||
return array(
|
||||
'queryKey' => 'optional string',
|
||||
'constraints' => 'optional map<string, wild>',
|
||||
'order' => 'optional order',
|
||||
) + $this->getPagerParamTypes();
|
||||
}
|
||||
|
||||
final protected function defineReturnType() {
|
||||
return 'map<string, wild>';
|
||||
}
|
||||
|
||||
final protected function execute(ConduitAPIRequest $request) {
|
||||
$engine = $this->newSearchEngine()
|
||||
->setViewer($request->getUser());
|
||||
|
||||
return $engine->buildConduitResponse($request);
|
||||
}
|
||||
|
||||
final public function getMethodDescription() {
|
||||
// TODO: We don't currently have a real viewer in this method.
|
||||
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||
|
||||
$engine = $this->newSearchEngine()
|
||||
->setViewer($viewer);
|
||||
|
||||
$query = $engine->newQuery();
|
||||
|
||||
$out = array();
|
||||
|
||||
$out[] = pht(<<<EOTEXT
|
||||
This is a standard **ApplicationSearch** method which will let you list, query,
|
||||
or search for objects.
|
||||
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
$out[] = pht(<<<EOTEXT
|
||||
Prebuilt Queries
|
||||
----------------
|
||||
|
||||
You can use a builtin or saved query as a starting point by passing it with
|
||||
`queryKey`. If you don't specify a `queryKey`, the query will start with no
|
||||
constraints.
|
||||
|
||||
For example, many applications have builtin queries like `"active"` or
|
||||
`"open"` to find only active or enabled results. To use a `queryKey`, specify
|
||||
it like this:
|
||||
|
||||
```lang=json
|
||||
{
|
||||
...
|
||||
"queryKey": "active",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
These builtin and saved queries are available:
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
$head_querykey = pht('Query Key');
|
||||
$head_name = pht('Name');
|
||||
$head_builtin = pht('Builtin');
|
||||
|
||||
$named_queries = $engine->loadAllNamedQueries();
|
||||
|
||||
$table = array();
|
||||
$table[] = "| {$head_querykey} | {$head_name} | {$head_builtin} |";
|
||||
$table[] = '|------------------|--------------|-----------------|';
|
||||
foreach ($named_queries as $named_query) {
|
||||
$key = $named_query->getQueryKey();
|
||||
$name = $named_query->getQueryName();
|
||||
$builtin = $named_query->getIsBuiltin()
|
||||
? pht('Builtin')
|
||||
: pht('Custom');
|
||||
|
||||
$table[] = "| `{$key}` | {$name} | {$builtin} |";
|
||||
}
|
||||
$table = implode("\n", $table);
|
||||
$out[] = $table;
|
||||
|
||||
$out[] = pht(<<<EOTEXT
|
||||
You can also use **any** query you run via the web UI as a starting point. You
|
||||
can find the key for a query by examining the URI after running a normal
|
||||
search.
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
$out[] = pht(<<<EOTEXT
|
||||
Custom Constraints
|
||||
------------------
|
||||
|
||||
You can add custom constraints to the basic query by passing `constraints`.
|
||||
This will let you filter results (for example, show only results with a
|
||||
certain state, status, or owner).
|
||||
|
||||
Specify constraints like this:
|
||||
|
||||
```lang=json
|
||||
{
|
||||
...
|
||||
"constraints": {
|
||||
"authorPHIDs": ["PHID-USER-1111", "PHID-USER-2222"],
|
||||
"statuses": ["open", "closed"]
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
If you specify both a `queryKey` and `constraints`, the basic query
|
||||
configuration will be applied first as a starting point, then any additional
|
||||
values in `constraints` will be applied, overwriting the defaults from the
|
||||
original query.
|
||||
|
||||
This API endpoint supports these constraints:
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
$head_key = pht('Key');
|
||||
$head_label = pht('Label');
|
||||
$head_type = pht('Type');
|
||||
$head_desc = pht('Description');
|
||||
|
||||
$fields = $engine->getSearchFieldsForConduit();
|
||||
|
||||
$table = array();
|
||||
$table[] = "| {$head_key} | {$head_label} | {$head_type} | {$head_desc} |";
|
||||
$table[] = '|-------------|---------------|--------------|--------------|';
|
||||
foreach ($fields as $field) {
|
||||
$key = $field->getKey();
|
||||
$label = $field->getLabel();
|
||||
|
||||
// TODO: Support generating and surfacing this information.
|
||||
$type = pht('TODO');
|
||||
$description = pht('TODO');
|
||||
|
||||
$table[] = "| `{$key}` | **{$label}** | `{$type}` | {$description}";
|
||||
}
|
||||
$table = implode("\n", $table);
|
||||
$out[] = $table;
|
||||
|
||||
|
||||
$out[] = pht(<<<EOTEXT
|
||||
Result Order
|
||||
------------
|
||||
|
||||
Use `order` to choose an ordering for the results. Either specify a single
|
||||
key from the builtin orders (these are a set of meaningful, high-level,
|
||||
human-readable orders) or specify a list of low-level columns.
|
||||
|
||||
To use a high-level order, choose a builtin order from the table below
|
||||
and specify it like this:
|
||||
|
||||
```lang=json
|
||||
{
|
||||
...
|
||||
"order": "newest",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
These builtin orders are available:
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
$head_builtin = pht('Builtin Order');
|
||||
$head_description = pht('Description');
|
||||
$head_columns = pht('Columns');
|
||||
|
||||
$orders = $query->getBuiltinOrders();
|
||||
|
||||
$table = array();
|
||||
$table[] = "| {$head_builtin} | {$head_description} | {$head_columns} |";
|
||||
$table[] = '|-----------------|---------------------|-----------------|';
|
||||
foreach ($orders as $key => $order) {
|
||||
$name = $order['name'];
|
||||
$columns = implode(', ', $order['vector']);
|
||||
$table[] = "| `{$key}` | {$name} | {$columns} |";
|
||||
}
|
||||
$table = implode("\n", $table);
|
||||
$out[] = $table;
|
||||
|
||||
$out[] = pht(<<<EOTEXT
|
||||
You can choose a low-level column order instead. This is an advanced feature.
|
||||
|
||||
In your custom order: each column may only be specified once; each column may
|
||||
be prefixed with "-" to invert the order; the last column must be unique; and
|
||||
no column other than the last may be unique.
|
||||
|
||||
To use a low-level order, choose a sequence of columns and specify them like
|
||||
this:
|
||||
|
||||
```lang=json
|
||||
{
|
||||
...
|
||||
"order": ["color", "-name", "id"],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
These low-level columns are available:
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
$head_column = pht('Column Key');
|
||||
$head_unique = pht('Unique');
|
||||
|
||||
$columns = $query->getOrderableColumns();
|
||||
|
||||
$table = array();
|
||||
$table[] = "| {$head_column} | {$head_unique} |";
|
||||
$table[] = '|----------------|----------------|';
|
||||
foreach ($columns as $key => $column) {
|
||||
$unique = idx($column, 'unique')
|
||||
? pht('Yes')
|
||||
: pht('No');
|
||||
|
||||
$table[] = "| `{$key}` | {$unique} |";
|
||||
}
|
||||
$table = implode("\n", $table);
|
||||
$out[] = $table;
|
||||
|
||||
|
||||
$out[] = pht(<<<EOTEXT
|
||||
Result Format
|
||||
-------------
|
||||
|
||||
The result format is a dictionary with several fields:
|
||||
|
||||
- `data`: Contains the actual results, as a list of dictionaries.
|
||||
- `query`: Details about the query which was issued.
|
||||
- `cursor`: Information about how to issue another query to get the next
|
||||
(or previous) page of results. See "Paging and Limits" below.
|
||||
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
$out[] = pht(<<<EOTEXT
|
||||
Fields
|
||||
------
|
||||
|
||||
The `data` field of the result contains a list of results. Each result has
|
||||
some metadata and a `fields` key, which contains the primary object fields.
|
||||
|
||||
For example, the results may look something like this:
|
||||
|
||||
```lang=json
|
||||
{
|
||||
...
|
||||
"data": [
|
||||
{
|
||||
"id": 123,
|
||||
"phid": "PHID-WXYZ-1111",
|
||||
"fields": {
|
||||
"name": "First Example Object",
|
||||
"authorPHID": "PHID-USER-2222"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 124,
|
||||
"phid": "PHID-WXYZ-3333",
|
||||
"fields": {
|
||||
"name": "Second Example Object",
|
||||
"authorPHID": "PHID-USER-4444"
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
This result structure is standardized across all search methods, but the
|
||||
available fields differ from application to application.
|
||||
|
||||
These are the fields available on this object type:
|
||||
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
$specs = $engine->getAllConduitFieldSpecifications();
|
||||
|
||||
$table = array();
|
||||
$table[] = "| {$head_key} | {$head_type} | {$head_description} |";
|
||||
$table[] = '|-------------|--------------|---------------------|';
|
||||
foreach ($specs as $key => $spec) {
|
||||
$type = idx($spec, 'type');
|
||||
$description = idx($spec, 'description');
|
||||
$table[] = "| `{$key}` | `{$type}` | {$description} |";
|
||||
}
|
||||
$table = implode("\n", $table);
|
||||
$out[] = $table;
|
||||
|
||||
$out[] = pht(<<<EOTEXT
|
||||
Paging and Limits
|
||||
-----------------
|
||||
|
||||
Queries are limited to returning 100 results at a time. If you want fewer
|
||||
results than this, you can use `limit` to specify a smaller limit.
|
||||
|
||||
If you want more results, you'll need to make additional queries to retrieve
|
||||
more pages of results.
|
||||
|
||||
The result structure contains a `cursor` key with information you'll need in
|
||||
order to fetch the next page. After an initial query, it will usually look
|
||||
something like this:
|
||||
|
||||
```lang=json
|
||||
{
|
||||
...
|
||||
"cursor": {
|
||||
"limit": 100,
|
||||
"after": "1234",
|
||||
"before": null,
|
||||
"order": null
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The `limit` and `order` fields are describing the effective limit and order the
|
||||
query was executed with, and are usually not of much interest. The `after` and
|
||||
`before` fields give you cursors which you can pass when making another API
|
||||
call in order to get the next (or previous) page of results.
|
||||
|
||||
To get the next page of results, repeat your API call with all the same
|
||||
parameters as the original call, but pass the `after` cursor you received from
|
||||
the first call in the `after` parameter when making the second call.
|
||||
|
||||
If you do things correctly, you should get the second page of results, and
|
||||
a cursor structure like this:
|
||||
|
||||
```lang=json
|
||||
{
|
||||
...
|
||||
"cursor": {
|
||||
"limit": 5,
|
||||
"after": "4567",
|
||||
"before": "7890",
|
||||
"order": null
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
You can now continue to the third page of results by passing the new `after`
|
||||
cursor to the `after` parameter in your third call, or return to the previous
|
||||
page of results by passing the `before` cursor to the `before` parameter. This
|
||||
might be useful if you are rendering a web UI for a user and want to provide
|
||||
"Next Page" and "Previous Page" links.
|
||||
|
||||
If `after` is `null`, there is no next page of results available. Likewise,
|
||||
if `before` is `null`, there are no previous results available.
|
||||
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
$out = implode("\n\n", $out);
|
||||
return $out;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorLiskSearchEngineExtension
|
||||
extends PhabricatorSearchEngineExtension {
|
||||
|
||||
const EXTENSIONKEY = 'lisk';
|
||||
|
||||
public function isExtensionEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getExtensionName() {
|
||||
return pht('Lisk Builtin Properties');
|
||||
}
|
||||
|
||||
public function supportsObject($object) {
|
||||
if (!($object instanceof LiskDAO)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$object->getConfigOption(LiskDAO::CONFIG_TIMESTAMPS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFieldSpecificationsForConduit($object) {
|
||||
return array(
|
||||
'dateCreated' => array(
|
||||
'type' => 'int',
|
||||
'description' => pht(
|
||||
'Epoch timestamp when the object was created.'),
|
||||
),
|
||||
'dateModified' => array(
|
||||
'type' => 'int',
|
||||
'description' => pht(
|
||||
'Epoch timestamp when the object was last updated.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getFieldValuesForConduit($object) {
|
||||
return array(
|
||||
'dateCreated' => (int)$object->getDateCreated(),
|
||||
'dateModified' => (int)$object->getDateModified(),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorSearchEngineExtension extends Phobject {
|
||||
|
||||
private $viewer;
|
||||
|
||||
final public function getExtensionKey() {
|
||||
return $this->getPhobjectClassConstant('EXTENSIONKEY');
|
||||
}
|
||||
|
||||
final public function setViewer($viewer) {
|
||||
$this->viewer = $viewer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getViewer() {
|
||||
return $this->viewer;
|
||||
}
|
||||
|
||||
abstract public function isExtensionEnabled();
|
||||
abstract public function getExtensionName();
|
||||
abstract public function supportsObject($object);
|
||||
|
||||
public function getFieldSpecificationsForConduit($object) {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function getFieldValuesForConduit($object) {
|
||||
return array();
|
||||
}
|
||||
|
||||
final public static function getAllExtensions() {
|
||||
return id(new PhutilClassMapQuery())
|
||||
->setAncestorClass(__CLASS__)
|
||||
->setUniqueMethod('getExtensionKey')
|
||||
->execute();
|
||||
}
|
||||
|
||||
final public static function getAllEnabledExtensions() {
|
||||
$extensions = self::getAllExtensions();
|
||||
|
||||
foreach ($extensions as $key => $extension) {
|
||||
if (!$extension->isExtensionEnabled()) {
|
||||
unset($extensions[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorSearchEngineExtensionModule
|
||||
extends PhabricatorConfigModule {
|
||||
|
||||
public function getModuleKey() {
|
||||
return 'searchengine';
|
||||
}
|
||||
|
||||
public function getModuleName() {
|
||||
return pht('SearchEngine Extensions');
|
||||
}
|
||||
|
||||
public function renderModuleStatus(AphrontRequest $request) {
|
||||
$viewer = $request->getViewer();
|
||||
|
||||
$extensions = PhabricatorSearchEngineExtension::getAllExtensions();
|
||||
|
||||
$rows = array();
|
||||
foreach ($extensions as $extension) {
|
||||
$rows[] = array(
|
||||
$extension->getExtensionKey(),
|
||||
get_class($extension),
|
||||
$extension->getExtensionName(),
|
||||
$extension->isExtensionEnabled()
|
||||
? pht('Yes')
|
||||
: pht('No'),
|
||||
);
|
||||
}
|
||||
|
||||
$table = id(new AphrontTableView($rows))
|
||||
->setHeaders(
|
||||
array(
|
||||
pht('Key'),
|
||||
pht('Class'),
|
||||
pht('Name'),
|
||||
pht('Enabled'),
|
||||
))
|
||||
->setColumnClasses(
|
||||
array(
|
||||
null,
|
||||
null,
|
||||
'wide pri',
|
||||
null,
|
||||
));
|
||||
|
||||
return id(new PHUIObjectBoxView())
|
||||
->setHeaderText(pht('SearchEngine Extensions'))
|
||||
->setTable($table);
|
||||
}
|
||||
|
||||
}
|
|
@ -205,6 +205,14 @@ abstract class PhabricatorSearchField extends Phobject {
|
|||
return $value;
|
||||
}
|
||||
|
||||
public function getValueExistsInConduitRequest(array $constraints) {
|
||||
return array_key_exists($this->getKey(), $constraints);
|
||||
}
|
||||
|
||||
public function readValueFromConduitRequest(array $constraints) {
|
||||
return idx($constraints, $this->getKey());
|
||||
}
|
||||
|
||||
|
||||
/* -( Rendering Controls )------------------------------------------------- */
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorSpacesSearchEngineExtension
|
||||
extends PhabricatorSearchEngineExtension {
|
||||
|
||||
const EXTENSIONKEY = 'spaces';
|
||||
|
||||
public function isExtensionEnabled() {
|
||||
return PhabricatorApplication::isClassInstalled(
|
||||
'PhabricatorSpacesApplication');
|
||||
}
|
||||
|
||||
public function getExtensionName() {
|
||||
return pht('Support for Spaces');
|
||||
}
|
||||
|
||||
public function supportsObject($object) {
|
||||
return ($object instanceof PhabricatorSpacesInterface);
|
||||
}
|
||||
|
||||
public function getFieldSpecificationsForConduit($object) {
|
||||
return array(
|
||||
'spacePHID' => array(
|
||||
'type' => 'phid?',
|
||||
'description' => pht(
|
||||
'PHID of the policy space this object is part of.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getFieldValuesForConduit($object) {
|
||||
return array(
|
||||
'spacePHID' => $object->getSpacePHID(),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue