APIv4 - Add rudimentary support for groupBy
authorColeman Watts <coleman@civicrm.org>
Sat, 21 Mar 2020 23:48:24 +0000 (19:48 -0400)
committerColeman Watts <coleman@civicrm.org>
Wed, 1 Apr 2020 21:10:19 +0000 (17:10 -0400)
Civi/Api4/Generic/DAOGetAction.php
Civi/Api4/Query/Api4SelectQuery.php
ang/api4Explorer/Explorer.html
ang/api4Explorer/Explorer.js

index 100173d9628da9a5111c692bfd193425a97a473f..ae29e3992a3f116bbddb20844e3e1d23cdb6fe4f 100644 (file)
@@ -44,6 +44,13 @@ class DAOGetAction extends AbstractGetAction {
    */
   protected $select = [];
 
+  /**
+   * Field(s) by which to group the results.
+   *
+   * @var array
+   */
+  protected $groupBy = [];
+
   public function _run(Result $result) {
     $this->setDefaultWhereClause();
     $this->expandSelectClauseWildcards();
@@ -63,4 +70,29 @@ class DAOGetAction extends AbstractGetAction {
     return $result;
   }
 
+  /**
+   * @return array
+   */
+  public function getGroupBy(): array {
+    return $this->groupBy;
+  }
+
+  /**
+   * @param array $groupBy
+   * @return $this
+   */
+  public function setGroupBy(array $groupBy) {
+    $this->groupBy = $groupBy;
+    return $this;
+  }
+
+  /**
+   * @param string $field
+   * @return $this
+   */
+  public function addGroupBy(string $field) {
+    $this->groupBy[] = $field;
+    return $this;
+  }
+
 }
index de08a7b1528f3aee38c57fb6682e73bb63f7ef56..4e7082a4dc4b8e56ee25eac40d5eb195c51987bf 100644 (file)
@@ -60,6 +60,11 @@ class Api4SelectQuery extends SelectQuery {
    */
   public $debugOutput = NULL;
 
+  /**
+   * @var array
+   */
+  public $groupBy = [];
+
   /**
    * @param \Civi\Api4\Generic\DAOGetAction $apiGet
    */
@@ -68,6 +73,7 @@ class Api4SelectQuery extends SelectQuery {
     $this->checkPermissions = $apiGet->getCheckPermissions();
     $this->select = $apiGet->getSelect();
     $this->where = $apiGet->getWhere();
+    $this->groupBy = $apiGet->getGroupBy();
     $this->orderBy = $apiGet->getOrderBy();
     $this->limit = $apiGet->getLimit();
     $this->offset = $apiGet->getOffset();
@@ -100,6 +106,7 @@ class Api4SelectQuery extends SelectQuery {
     $this->buildWhereClause();
     $this->buildOrderBy();
     $this->buildLimit();
+    $this->buildGroupBy();
     return $this->query->toSQL();
   }
 
@@ -115,20 +122,21 @@ class Api4SelectQuery extends SelectQuery {
       $this->debugOutput['sql'][] = $sql;
     }
     $query = \CRM_Core_DAO::executeQuery($sql);
-
+    $i = 0;
     while ($query->fetch()) {
+      $id = $query->id ?? $i++;
       if (in_array('row_count', $this->select)) {
         $results[]['row_count'] = (int) $query->c;
         break;
       }
-      $results[$query->id] = [];
+      $results[$id] = [];
       foreach ($this->select as $alias) {
         $returnName = $alias;
         if ($this->isOneToOneField($alias)) {
           $alias = str_replace('.', '_', $alias);
-          $results[$query->id][$returnName] = property_exists($query, $alias) ? $query->$alias : NULL;
+          $results[$id][$returnName] = property_exists($query, $alias) ? $query->$alias : NULL;
         }
-      };
+      }
     }
     $event = new PostSelectQueryEvent($results, $this);
     \Civi::dispatcher()->dispatch(Events::POST_SELECT_QUERY, $event);
@@ -145,8 +153,10 @@ class Api4SelectQuery extends SelectQuery {
       return;
     }
     else {
-      // Always select id field
-      $this->select = array_merge(['id'], $this->select);
+      // Always select ID (unless we're doing groupBy).
+      if (!$this->groupBy) {
+        $this->select = array_merge(['id'], $this->select);
+      }
 
       // Expand wildcards in joins (the api wrapper already expanded non-joined wildcards)
       $wildFields = array_filter($this->select, function($item) {
@@ -209,6 +219,20 @@ class Api4SelectQuery extends SelectQuery {
     }
   }
 
+  /**
+   *
+   */
+  protected function buildGroupBy() {
+    foreach ($this->groupBy as $field) {
+      if ($this->isOneToOneField($field) && $this->getField($field)) {
+        $this->query->groupBy($field['sql_name']);
+      }
+      else {
+        throw new \API_Exception("Invalid field. Cannot group by $field");
+      }
+    }
+  }
+
   /**
    * Recursively validate and transform a branch or leaf clause array to SQL.
    *
index 659917ecb2fe8a459cc27c658148f563459784ef..ad35e3810317ba95a01adadc356777d2bbdc0d52 100644 (file)
             <div class="api4-input form-inline">
               <input class="collapsible-optgroups form-control" ng-model="controls[name]" crm-ui-select="{formatResult: formatSelect2Item, formatSelection: formatSelect2Item, data: fieldList(name), placeholder: ts('Add %1', {1: name.slice(0, -1)})}"/>
             </div>
-          </fieldset>
+          </fieldset><fieldset ng-if="availableParams.groupBy" ng-mouseenter="help('groupBy', availableParams.groupBy)" ng-mouseleave="help()">
+          <legend>groupBy<span class="crm-marker" ng-if="availableParams.groupBy.required"> *</span></legend>
+          <div ng-model="params.groupBy" ui-sortable="{axis: 'y'}">
+            <div class="api4-input form-inline" ng-repeat="(pos, field) in params.groupBy">
+              <i class="crm-i fa-arrows"></i>
+              <input class="collapsible-optgroups form-control" ng-model="params.groupBy[pos]" crm-ui-select="{data: fieldsAndJoins, allowClear: true, placeholder: 'Field'}" />
+            </div>
+          </div>
+          <div class="api4-input form-inline">
+            <input class="collapsible-optgroups form-control" ng-model="controls.groupBy" crm-ui-select="{data: fieldsAndJoins}" placeholder="Add groupBy" />
+          </div>
+        </fieldset>
           <fieldset ng-if="availableParams.orderBy" ng-mouseenter="help('orderBy', availableParams.orderBy)" ng-mouseleave="help()">
             <legend>orderBy<span class="crm-marker" ng-if="availableParams.orderBy.required"> *</span></legend>
-            <div class="api4-input form-inline" ng-repeat="clause in params.orderBy">
-              <input class="collapsible-optgroups form-control" ng-model="clause[0]" crm-ui-select="{data: fieldsAndJoins, allowClear: true, placeholder: 'Field'}" />
-              <select class="form-control" ng-model="clause[1]">
-                <option value="ASC">ASC</option>
-                <option value="DESC">DESC</option>
-              </select>
+            <div ng-model="params.orderBy" ui-sortable="{axis: 'y'}">
+              <div class="api4-input form-inline" ng-repeat="clause in params.orderBy">
+                <i class="crm-i fa-arrows"></i>
+                <input class="collapsible-optgroups form-control" ng-model="clause[0]" crm-ui-select="{data: fieldsAndJoins, allowClear: true, placeholder: 'Field'}" />
+                <select class="form-control" ng-model="clause[1]">
+                  <option value="ASC">ASC</option>
+                  <option value="DESC">DESC</option>
+                </select>
+              </div>
             </div>
             <div class="api4-input form-inline">
               <input class="collapsible-optgroups form-control" ng-model="controls.orderBy" crm-ui-select="{data: fieldsAndJoins}" placeholder="Add orderBy" />
             </div>
           </fieldset>
+
           <fieldset ng-if="availableParams.chain" ng-mouseenter="help('chain', availableParams.chain)" ng-mouseleave="help()">
             <legend>chain</legend>
             <div class="api4-input form-inline" ng-repeat="clause in params.chain" api4-exp-chain="clause" entities="entities" main-entity="entity" >
index 09db7d1d02a2023fee9ea88a87af0f7e462f5bf5..f75443dcf49bc13485847d992db1d7a84f8e92f2 100644 (file)
     };
 
     $scope.isSpecial = function(name) {
-      var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain'];
+      var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain', 'groupBy'];
       return _.contains(specialParams, name);
     };
 
               deep: format === 'json'
             });
           }
-          if (typeof objectParams[name] !== 'undefined') {
+          if (typeof objectParams[name] !== 'undefined' || name === 'groupBy') {
             $scope.$watch('params.' + name, function(values) {
               // Remove empty values
               _.each(values, function(clause, index) {
               var field = value;
               $timeout(function() {
                 if (field) {
-                  var defaultOp = _.cloneDeep(objectParams[name]);
-                  if (name === 'chain') {
-                    var num = $scope.params.chain.length;
-                    defaultOp[0] = field;
-                    field = 'name_me_' + num;
+                  if (name === 'groupBy') {
+                    $scope.params[name].push(field);
+                  } else {
+                    var defaultOp = _.cloneDeep(objectParams[name]);
+                    if (name === 'chain') {
+                      var num = $scope.params.chain.length;
+                      defaultOp[0] = field;
+                      field = 'name_me_' + num;
+                    }
+                    $scope.params[name].push([field, defaultOp]);
                   }
-                  $scope.params[name].push([field, defaultOp]);
                   $scope.controls[name] = null;
                 }
               });