diff --git a/app/src/Controller/ApiV2Controller.php b/app/src/Controller/ApiV2Controller.php index e40f30841..8bb133898 100644 --- a/app/src/Controller/ApiV2Controller.php +++ b/app/src/Controller/ApiV2Controller.php @@ -41,7 +41,8 @@ class ApiV2Controller extends AppController { use \App\Lib\Traits\LabeledLogTrait; - + use \App\Lib\Traits\IndexQueryTrait; + /** * Perform Cake Controller initialization. * @@ -77,6 +78,8 @@ public function initialize(): void { public function add() { // $this->name = Models $modelsName = $this->name; + // $table = the actual table object + $table = $this->$modelsName; // $tableName = models $tableName = $this->tableName; @@ -144,15 +147,20 @@ public function beforeRender(\Cake\Event\EventInterface $event) { public function delete($id) { // $this->name = Models (ie: from ModelsTable) $modelsName = $this->name; - + // $table = the actual table object + $table = $this->$modelsName; + // $tableName = models + $tableName = $table->getTable(); + + // Make sure the requested object exists try { - $obj = $this->$modelsName->findById($id)->firstOrFail(); - + $obj = $table->findById($id)->firstOrFail(); + // XXX document AR-CO-1 when we implement hard delete/changelog // note similar logic in StandardController - $this->$modelsName->deleteOrFail($obj); - + $table->deleteOrFail($obj); + if(method_exists($obj, "isReadOnly") && $obj->isReadOnly()) { throw new BadRequestException(__d('error', 'edit.readonly')); } @@ -184,10 +192,12 @@ public function delete($id) { public function edit($id) { // $this->name = Models (ie: from ModelsTable) $modelsName = $this->name; + // $table = the actual table object + $table = $this->$modelsName; // $tableName = models - $tableName = $this->$modelsName->getTable(); + $tableName = $table->getTable(); - $query = $this->$modelsName->findById($id); + $query = $table->findById($id); try { // Pull the current record @@ -203,14 +213,14 @@ public function edit($id) { throw new BadRequestException(__d('error', 'api.object', [$modelsName])); } - $obj = $this->$modelsName->patchEntity($obj, $json[$modelsName]); + $obj = $table->patchEntity($obj, $json[$modelsName]); - $this->$modelsName->saveOrFail($obj); + $table->saveOrFail($obj); // Trigger provisioning, letting errors bubble up (AR-GMR-5) - if(method_exists($this->$modelsName, "requestProvisioning")) { + if(method_exists($table, "requestProvisioning")) { $this->llog('rule', "AR-GMR-5 Requesting provisioning for $modelsName " . $obj->id); - $this->$modelsName->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic); + $table->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic); } // Let the view render @@ -224,7 +234,6 @@ public function edit($id) { $err = $this->exceptionToError($e); $this->llog('debug', $err); - $results[] = ['error' => $err]; throw new BadRequestException($this->exceptionToError($e)); } @@ -293,47 +302,12 @@ public function generateApiKey(string $id) { public function index() { // $modelsName = Models $modelsName = $this->name; - - $query = $this->$modelsName->find(); - - // PrimaryLinkTrait - $link = $this->getPrimaryLink(true); - - // We automatically allow API calls to be filtered on primary link - if(!empty($link->attr) && !empty($link->value)) { - $query = $query->where([$this->$modelsName->getAlias().'.'.$link->attr => $link->value]); - } + // $table = the actual table object + $table = $this->$modelsName; - // This will produce a nested object which is very useful for vue integration - if($this->request->getQuery('extended') !== null) { - $modelContain = []; - $associations = $this->$modelsName->associations(); - foreach($associations->getByType(['BelongsTo']) as $a) { - $modelContain[] = $a->getClassName(); - } - - if(!empty($modelContain)) { - $query = $query->contain($modelContain); - } - } + // Construct the Query + $query = $this->getIndexQuery(); - if($modelsName == 'AuthenticationEvents') { - // Special case for filtering on authenticated identifier. There is a - // similar filter in AuthenticationEventsController::beforeFilter. - // If other special cases show up this should get refactored into a trait - // populated by the table (or something similar). - - if($this->getRequest()->getQuery('authenticated_identifier')) { - $query = $query->where(['authenticated_identifier' => \App\Lib\Util\StringUtilities::urlbase64decode($this->getRequest()->getQuery('authenticated_identifier'))]); - } else { - // We only allow unfiltered queries for platform users - - if(!$this->RegistryAuth->isPlatformAdmin()) { - throw new \InvalidArgumentException(__d('error', 'input.notprov', 'authenticated_identifier')); - } - } - } - // This magically makes REST calls paginated... can use eg direction=, // sort=, limit=, page= $this->set($this->tableName, $this->paginate($query)); @@ -351,14 +325,16 @@ public function index() { public function view($id = null) { // $this->name = Models $modelsName = $this->name; + // $table = the actual table object + $table = $this->$modelsName; // $tableName = models - $tableName = $this->$modelsName->getTable(); + $tableName = $table->getTable(); if(empty($id)) { throw new InvalidArgumentException(__d('error', 'notprov', ['id'])); } - $obj = $this->$modelsName->findById($id)->firstOrFail(); + $obj = $table->findById($id)->firstOrFail(); $this->set($tableName, [$obj]); diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 0fbc1dc00..ba4d7d7f2 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -37,6 +37,8 @@ use \App\Lib\Util\StringUtilities; class StandardController extends AppController { + use \App\Lib\Traits\IndexQueryTrait; + // Pagination defaults should be set in each controller public $pagination = []; @@ -565,64 +567,17 @@ public function index() { $table = $this->$modelsName; // $tableName = models $tableName = $table->getTable(); - // PrimaryLinkTrait - $link = $this->getPrimaryLink(true); - - // AutoViewVarsTrait - $this->populateAutoViewVars(); - // Initialize the Query Object - $query = $table->find($this->paginate['finder'] ?? 'all'); - // Get a pointer to my expressions list - $newexp = $query->newExpr(); - - if(!empty($link->attr)) { - // If a link attribute is defined but no value is provided, then query - // where the link attribute is NULL - // "all" is the default finder. But since we are utilizing the paginator here, we will check the configuration - // for any custom finder. - $newexp->add([$table->getAlias().'.'.$link->attr => $link->value]); - } - - // QueryModificationTrait - if(method_exists($table, 'getIndexContains') - && $table->getIndexContains()) { - $query->contain($table->getIndexContains()); - } - - // SearchFilterTrait - if(method_exists($table, 'getSearchableAttributes')) { - $searchableAttributes = $table->getSearchableAttributes($this->name, - $this->viewBuilder()->getVar('vv_tz')); - - if(!empty($searchableAttributes)) { - // Here we iterate over the attributes, and we add a new where clause for each one - foreach($searchableAttributes as $attribute => $options) { - // Add the Join Clauses - $query = $table->addJoins($query, $attribute, $this->request); - - // Construct and apply the where Clause - if(!empty($this->request->getQuery($attribute))) { - $newexp = $table->expressionsConstructor($query, $newexp, $attribute, $this->request->getQuery($attribute)); - } elseif (!empty($this->request->getQuery($attribute . '_starts_at')) - || !empty($this->request->getQuery($attribute . '_ends_at'))) { - $search_date = []; - // We allow empty for dates since we might refer to infinity (from whenever or to always) - $search_date[] = $this->request->getQuery($attribute . '_starts_at') ?? ''; - $search_date[] = $this->request->getQuery($attribute . '_ends_at') ?? ''; - $newexp = $table->expressionsConstructor($query, $newexp, $attribute, $search_date); - } - } - - $this->set('vv_searchable_attributes', $searchableAttributes); - } - } - - $query = $query->where($newexp); + // Construct the Query + $query = $this->getIndexQuery(); + // Fetch the data and paginate $resultSet = $this->paginate($query); - + + // Pass vars to the View $this->set($tableName, $resultSet); $this->set('vv_permission_set', $this->RegistryAuth->calculatePermissionsForResultSet($resultSet)); - + // AutoViewVarsTrait + $this->populateAutoViewVars(); + // Default index view title is model name [$title, , ] = StringUtilities::entityAndActionToTitle($resultSet, $modelsName, 'index'); $this->set('vv_title', $title); @@ -769,7 +724,7 @@ protected function populateAutoViewVars(object $obj=null) { break; default: // XXX I18n? and in match? - throw new \LogicException('Unknonwn Auto View Var Type {0}', [$avv['type']]); + throw new \LogicException('Unknonwn Auto View Var Type {0}', $avv['type']); break; } } diff --git a/app/src/Lib/Traits/IndexQueryTrait.php b/app/src/Lib/Traits/IndexQueryTrait.php new file mode 100644 index 000000000..3c6d73927 --- /dev/null +++ b/app/src/Lib/Traits/IndexQueryTrait.php @@ -0,0 +1,171 @@ +name = Models + $modelsName = $this->name; + // $table = the actual table object + $table = $this->$modelsName; + // Initialize the containClause + $containClause = []; + + // Get whatever the table configuration has + if(method_exists($table, 'getIndexContains') + && $table->getIndexContains()) { + $containClause = $table->getIndexContains(); + } + + // Examples: + // 1. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10&extended=PrimaryName,EmailAddresses,Identifiers + // 2. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10&extended=on + // 1. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10 + if($this->request->is('restful')|| $this->request->is('ajax')) { + // Restfull and ajax do not include the IndexContains by default. + $containClause = []; + // Set the extended query param to `on` in order to fetch the indexContains + if( + $this->request->getQuery('extended') && + filter_var($this->request->getQuery('extended'), FILTER_VALIDATE_BOOLEAN) + ) { + $containClause = $table->getIndexContains(); + } elseif($this->request->getQuery('extended') + && \is_string($this->request->getQuery('extended'))) { + // This is a string. We will parse the csv and continue + $associations = $table->associations(); + $containQueryList = str_getcsv($this->request->getQuery('extended')); + foreach($associations->getIterator() as $a) { + if(\in_array($a->getName(), $containQueryList, true)) { + $containClause[] = $a->getName(); + } + } + } + } + + return empty($containClause) ? $query : $query->contain($containClause); + } + + /** + * Build the Index Query + * + * + * @return object Cake ORM Query object + * @since COmanage Registry v5.0.0 + */ + public function getIndexQuery(): object { + // $this->name = Models + $modelsName = $this->name; + // $table = the actual table object + $table = $this->$modelsName; + // PrimaryLinkTrait + $link = $this->getPrimaryLink(true); + // Initialize the Query Object + $query = $table->find($this->paginate['finder'] ?? 'all'); + // Get a pointer to my expressions list + $newexp = $query->newExpr(); + // The searchable attributes can have an AND or an OR conjuction. The first one is used from the filtering block + // while the second one from the picker vue module. + $newexp = $newexp->setConjunction(true ? 'AND' : 'OR'); + + if(!empty($link->attr) && !empty($link->value)) { + // If a link attribute is defined but no value is provided, then query + // where the link attribute is NULL + // "all" is the default finder. But since we are utilizing the paginator here, we will check the configuration + // for any custom finder. + $query = $query->where([$table->getAlias().'.'.$link->attr => $link->value]); + } + + // Get Associated Model Data + $query = $this->constructGetIndexContains($query); + + // SearchFilterTrait + if(method_exists($table, 'getSearchableAttributes')) { + $searchableAttributes = $table->getSearchableAttributes($this->name, + $this->viewBuilder()->getVar('vv_tz')); + + if(!empty($searchableAttributes)) { + $this->set('vv_searchable_attributes', $searchableAttributes); + + // Here we iterate over the attributes, and we add a new where clause for each one + foreach($searchableAttributes as $attribute => $options) { + // Add the Join Clauses + $query = $table->addJoins($query, $attribute, $this->request); + + // Construct and apply the where Clause + if(!empty($this->request->getQuery($attribute))) { + $newexp = $table->expressionsConstructor($query, $newexp, $attribute, $this->request->getQuery($attribute)); + } elseif (!empty($this->request->getQuery($attribute . '_starts_at')) + || !empty($this->request->getQuery($attribute . '_ends_at'))) { + $search_date = []; + // We allow empty for dates since we might refer to infinity (from whenever or to always) + $search_date[] = $this->request->getQuery($attribute . '_starts_at') ?? ''; + $search_date[] = $this->request->getQuery($attribute . '_ends_at') ?? ''; + $newexp = $table->expressionsConstructor($query, $newexp, $attribute, $search_date); + } + } + + // Append the new conditions + $query = $query->where($newexp); + } + } + + // Special Authenticated Identifier filtering + if($modelsName == 'AuthenticationEvents') { + // Special case for filtering on authenticated identifier. There is a + // todo: + // similar filter in AuthenticationEventsController::beforeFilter. + // If other special cases show up this should get refactored into a trait + // populated by the table (or something similar). + + if($this->getRequest()->getQuery('authenticated_identifier')) { + $query = $query->where(['authenticated_identifier' => \App\Lib\Util\StringUtilities::urlbase64decode($this->getRequest()->getQuery('authenticated_identifier'))]); + } else { + // We only allow unfiltered queries for platform users + + if(!$this->RegistryAuth->isPlatformAdmin()) { + throw new \InvalidArgumentException(__d('error', 'input.notprov', 'authenticated_identifier')); + } + } + } + + return $query; + } +} \ No newline at end of file