1 /*
  2  * Copyright (c) 2013-2015 LabKey Corporation
  3  *
  4  * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
  5  */
  6 (function() {
  7     /**
  8      * @private
  9      */
 10     var validateFilter = function(filter) {
 11         var filterObj = {};
 12         if (filter instanceof LABKEY.Query.Filter || filter.getColumnName) {
 13             filterObj.fieldKey = LABKEY.FieldKey.fromString(filter.getColumnName()).getParts();
 14             filterObj.value = filter.getValue();
 15             filterObj.type = filter.getFilterType().getURLSuffix();
 16             return filterObj;
 17         }
 18 
 19         //If filter isn't a LABKEY.Query.Filter or LABKEY.Filter, then it's probably a raw object.
 20         if (filter.fieldKey) {
 21             filter.fieldKey = validateFieldKey(filter.fieldKey);
 22         } else {
 23             throw new Error('All filters must have a "fieldKey" attribute.');
 24         }
 25 
 26         if (!filter.fieldKey) {
 27             throw new Error("Filter fieldKeys must be valid FieldKeys");
 28         }
 29 
 30         if (!filter.type) {
 31             throw new Error('All filters must have a "type" attribute.');
 32         }
 33         return filter;
 34     };
 35 
 36     /**
 37      * @private
 38      */
 39     var _validateKey = function(key, keyClazz) {
 40         if (key instanceof keyClazz) {
 41             return key.getParts();
 42         }
 43 
 44         if (key instanceof Array) {
 45             return key;
 46         }
 47 
 48         if (typeof key === 'string') {
 49             return keyClazz.fromString(key).getParts();
 50         }
 51 
 52         return false;
 53     };
 54 
 55     /**
 56      * @private
 57      */
 58     var validateSchemaKey = function(schemaKey) {
 59         return _validateKey(schemaKey, LABKEY.SchemaKey);
 60     };
 61 
 62     /**
 63      * @private
 64      */
 65     var validateFieldKey = function(fieldKey) {
 66         return _validateKey(fieldKey, LABKEY.FieldKey);
 67     };
 68 
 69     /**
 70      * @private
 71      */
 72     var validateSource = function(source) {
 73         if (!source || source == null) {
 74             throw new Error('A source is required for a GetData request.');
 75         }
 76 
 77         if (!source.type) {
 78             source.type = 'query';
 79         }
 80 
 81         if (!source.schemaName) {
 82             throw new Error('A schemaName is required.');
 83         }
 84 
 85         source.schemaName = validateSchemaKey(source.schemaName);
 86 
 87         if (!source.schemaName) {
 88             throw new Error('schemaName must be a FieldKey');
 89         }
 90 
 91         if (source.type === 'query') {
 92             if (!source.queryName || source.queryName == null) {
 93                 throw new Error('A queryName is required for getData requests with type = "query"');
 94             }
 95         } else if (source.type === 'sql') {
 96             if (!source.sql) {
 97                 throw new Error('sql is required if source.type = "sql"');
 98             }
 99         } else {
100             throw new Error('Unsupported source type.');
101         }
102     };
103 
104     /**
105      * @private
106      */
107     var validatePivot = function(pivot) {
108         if (!pivot.columns || pivot.columns == null) {
109             throw new Error('pivot.columns is required.');
110         }
111 
112         if (!pivot.columns instanceof Array) {
113             throw new Error('pivot.columns must be an array of fieldKeys.');
114         }
115 
116         for (var i = 0; i < pivot.columns.length; i++) {
117             pivot.columns[i] = validateFieldKey(pivot.columns[i]);
118 
119             if (!pivot.columns[i]) {
120                 throw new Error('pivot.columns must be an array of fieldKeys.');
121             }
122         }
123 
124         if (!pivot.by || pivot.by ==  null) {
125             throw new Error('pivot.by is required');
126         }
127 
128         pivot.by = validateFieldKey(pivot.by);
129 
130         if (!pivot.by === false) {
131             throw new Error('pivot.by must be a fieldKey.');
132         }
133     };
134 
135     /**
136      * @private
137      */
138     var validateTransform = function(transform) {
139         var i;
140 
141         // Issue 18138
142         if (!transform.type || transform.type !== 'aggregate') {
143             transform.type = 'aggregate';
144         }
145 
146         if (transform.groupBy && transform.groupBy != null) {
147             if (!transform.groupBy instanceof Array) {
148                 throw new Error('groupBy must be an array.');
149             }
150         }
151 
152 
153         if (transform.aggregates && transform.aggregates != null) {
154             if (!transform.aggregates instanceof Array) {
155                 throw new Error('aggregates must be an array.');
156             }
157 
158             for (i = 0; i < transform.aggregates.length; i++) {
159                 if (!transform.aggregates[i].fieldKey) {
160                     throw new Error('All aggregates must include a fieldKey.');
161                 }
162 
163                 transform.aggregates[i].fieldKey = validateFieldKey(transform.aggregates[i].fieldKey);
164 
165                 if (!transform.aggregates[i].fieldKey) {
166                     throw new Error('Aggregate fieldKeys must be valid fieldKeys');
167                 }
168 
169                 if (!transform.aggregates[i].type) {
170                     throw new Error('All aggregates must include a type.');
171                 }
172             }
173         }
174 
175         if (transform.filters && transform.filters != null) {
176             if (!transform.filters instanceof Array) {
177                 throw new Error('The filters of a transform must be an array.');
178             }
179 
180             for (i = 0; i < transform.filters.length; i++) {
181                 transform.filters[i] = validateFilter(transform.filters[i]);
182             }
183         }
184     };
185 
186     var validateGetDataConfig = function(config) {
187         if (!config || config === null || config === undefined) {
188             throw new Error('A config object is required for GetData requests.');
189         }
190 
191         var jsonData = {renderer: {}};
192         var i;
193         validateSource(config.source);
194 
195         // Shallow copy source so if the user adds unexpected properties to source the server doesn't throw errors.
196         jsonData.source = {
197             type: config.source.type,
198             schemaName: config.source.schemaName
199         };
200 
201         if (config.source.type === 'query') {
202             jsonData.source.queryName = config.source.queryName;
203         }
204 
205         if (config.source.type === 'sql') {
206             jsonData.source.sql = config.source.sql;
207         }
208 
209         if (config.transforms) {
210             if (!(config.transforms instanceof Array)) {
211                 throw new Error("transforms must be an array.");
212             }
213 
214             jsonData.transforms = config.transforms;
215             for (i = 0; i < jsonData.transforms.length; i++) {
216                 validateTransform(jsonData.transforms[i]);
217             }
218         }
219 
220         if (config.pivot) {
221             validatePivot(config.pivot);
222         }
223 
224         if (config.columns) {
225             if (!(config.columns instanceof Array)) {
226                 throw new Error('columns must be an array of FieldKeys.');
227             }
228 
229             for (i = 0; i < config.columns.length; i++) {
230                 config.columns[i] = validateFieldKey(config.columns[i]);
231 
232                 if (!config.columns[i]) {
233                     throw new Error('columns must be an array of FieldKeys.');
234                 }
235             }
236 
237             jsonData.renderer.columns = config.columns;
238         }
239 
240         if(config.hasOwnProperty('offset')){
241             jsonData.renderer.offset = config.offset;
242         }
243 
244         if(config.hasOwnProperty('includeDetailsColumn')){
245             jsonData.renderer.includeDetailsColumn = config.includeDetailsColumn;
246         }
247 
248         if(config.hasOwnProperty('maxRows')){
249             jsonData.renderer.maxRows = config.maxRows;
250         }
251 
252         if(config.sort){
253             if(!(config.sort instanceof Array)){
254                 throw new Error('sort must be an array.');
255             }
256 
257             for(i = 0; i < config.sort.length; i++){
258                 if(!config.sort[i].fieldKey){
259                     throw new Error("Each sort must specify a field key.");
260                 }
261 
262                 config.sort[i].fieldKey = validateFieldKey(config.sort[i].fieldKey);
263 
264                 if(!config.sort[i].fieldKey){
265                     throw new Error("Invalid field key specified for sort.");
266                 }
267 
268                 if(config.sort[i].dir){
269                     config.sort[i].dir = config.sort[i].dir.toUpperCase();
270                 }
271             }
272 
273             jsonData.renderer.sort = config.sort;
274         }
275 
276         return jsonData;
277     };
278 
279     /**
280      * @namespace GetData static class to access javascript APIs related to our GetData API.
281      */
282     LABKEY.Query.GetData = {
283         /**
284          * Used to get the raw data from a GetData request. Roughly equivalent to {@link LABKEY.Query.selectRows} or
285          * {@link LABKEY.Query.executeSql}, except it allows the user to pass the data through a series of transforms.
286          * @function
287          * @param {Object} config Required. An object which contains the following configuration properties:
288          * @param {Object} config.source Required. An object which contains parameters related to the source of the request.
289          * @param {String} config.source.type Required. A string with value set to either "query" or "sql". Indicates if the value is
290          *      "sql" then source.sql is required. If the value is "query" then source.queryName is required.
291          * @param {*} config.source.schemaName Required. The schemaName to use in the request. Can be a string, array of strings, or LABKEY.FieldKey.
292          * @param {String} config.source.queryName The queryName to use in the request. Required if source.type = "query".
293          * @param {String} config.source.sql The LabKey SQL to use in the request. Required if source.type = "sql".
294          * @param {String} config.source.containerPath The path to the target container to execute the GetData call in.
295          * @param {String} config.source.containerFilter Optional. The container filter to use in the request. See {@link LABKEY.Query.containerFilter}
296          *      for valid container filter types.
297          * @param {Object[]} config.transforms An array of objects with the following properties:
298          *              <ul>
299          *                  <li>
300          *                      <strong>pivot</strong>: {Object} Optional. An object with the following properties:
301          *                      <ul>
302          *                          <li>
303          *                              <strong>columns</strong>:
304          *                              {Array} The columns to pivot. Is an array containing strings, arrays of strings, and/or
305          *                              {@link LABKEY.FieldKey} objects.
306          *                          </li>
307          *                          <li>
308          *                              <strong>by</strong>:
309          *                              The column to pivot by. Can be an array of strings, a string, or a {@link LABKEY.FieldKey}
310          *                          </li>
311          *                      </ul>
312          *                  </li>
313          *                  <li>
314          *                      <strong>groupBy</strong>: {Object[]} An array of Objects. Each object can be a string, array of strings,
315          *                      or a {@link LABKEY.FieldKey}.
316          *                  </li>
317          *                  <li>
318          *                      <strong>aggregates</strong>: {Object[]} Optional. An array of objects with the following properties:
319          *                      <ul>
320          *                          <li>
321          *                              <strong>fieldKey</strong>:
322          *                              Required. The target column. Can be an array of strings, a string, or a {@link LABKEY.FieldKey}
323          *                          </li>
324          *                          <li><strong>type</strong>: {String} Required. The type of aggregate.</li>
325          *                          <li><strong>alias</strong>: {String} Required. The name to alias the aggregate as.</li>
326          *                          <li>
327          *                              <strong>metadata</strong>: {Object} An object containing the ColumnInfo metadata properties.
328          *                          </li>
329          *                      </ul>
330          *                  </li>
331          *                  <li>
332          *                      <strong>filters</strong>: {Object[]} Optional. An array containing  objects created with
333          *                  {@link LABKEY.Filter.create}, {@link LABKEY.Query.Filter} objects, or javascript objects with the following
334          *                  properties:
335          *                      <ul>
336          *                          <li>
337          *                              <strong>fieldKey</strong>: Required. Can be a string, array of strings, or a
338          *                          {@link LABKEY.FieldKey}
339          *                          </li>
340          *                          <li>
341          *                              <strong>type</strong>: Required. Can be a string or a type from {@link LABKEY.Filter#Types}
342          *                          </li>
343          *                          <li><strong>value</strong>: Optional depending on filter type. The value to filter on.</li>
344          *                      </ul>
345          *                  </li>
346          *              </ul>
347          * @param {Array} config.columns Optional. An array containing {@link LABKEY.FieldKey} objects, strings, or arrays of strings.
348          *      Used to specify which columns the user wants. The columns must match those returned from the last transform.
349          * @param {Integer} config.maxRows The maximum number of rows to return from the server (defaults to 100000). If you want
350          *      to return all possible rows, set this config property to -1.
351          * @param {Integer} config.offset The index of the first row to return from the server (defaults to 0). Use this along
352          *      with the maxRows config property to request pages of data.
353          * @param {Boolean} config.includeDetailsColumn Include the Details link column in the set of columns (defaults to false).
354          *      If included, the column will have the name "~~Details~~". The underlying table/query must support details
355          *      links or the column will be omitted in the response.
356          * @param {Object[]} config.sort Optional. Define how columns are sorted. An array of objects with the following properties:
357          *      <ul>
358          *          <li>
359          *              <strong>fieldKey</strong>: The field key of the column to sort. Can be a string, array of strings, or a
360          *               {@link LABKEY.FieldKey}
361          *          </li>
362          *          <li><strong>dir</strong>: {String} Optional. Can be 'ASC' or 'DESC', defaults to 'ASC'.</li>
363          *      </ul>
364          * @param {Function} config.success Required. A function to be executed when the GetData request completes
365          *      successfully. The function will
366          *      be passed a {@link LABKEY.Query.Response} object.
367          * @param {Function} config.failure Optional. If no failure function is provided the response is sent to the console
368          *      via console.error. If a function is provided the JSON response is passed to it as the only parameter.
369          * @returns {LABKEY.Ajax.request}
370          */
371         getRawData: function(config) {
372             var jsonData = validateGetDataConfig(config);
373             jsonData.renderer.type = 'json';
374 
375             var requestConfig = {
376                 method: 'POST',
377                 url: LABKEY.ActionURL.buildURL('query', 'getData', config.source.containerPath),
378                 jsonData: jsonData
379             };
380 
381             if (!config.failure) {
382                 requestConfig.failure = function(response, options) {
383                     if (response.status != 0) {
384                         var json = LABKEY.Utils.decode(response.responseText);
385                         console.error('Failure occurred during getData', json);
386                     }
387                 };
388             } else {
389                 requestConfig.failure = function(response, options) {
390                     var json = LABKEY.Utils.decode(response.responseText);
391                     config.failure(json);
392                 };
393             }
394 
395             if (!config.success) {
396                 throw new Error("A success callback is required.");
397             }
398 
399             if (!config.scope) {
400                 config.scope = this;
401             }
402 
403             requestConfig.success = function(response, options) {
404                 var json = LABKEY.Utils.decode(response.responseText);
405                 var wrappedResponse = new LABKEY.Query.Response(json);
406                 config.success.call(config.scope, wrappedResponse, response, options);
407             };
408 
409             return new LABKEY.Ajax.request(requestConfig);
410         }
411     };
412 })();
413 
414 /** docs for methods defined in dom/GetData.js - primarily here to ensure API docs get generated with combined core/dom versions */
415 
416 /**
417  * Used to render a queryWebPart around a response from GetData.
418  * @memberOf LABKEY.Query.GetData
419  * @function
420  * @static
421  * @name renderQueryWebPart
422  * @param {Object} config The config object for renderQueryWebpart is nearly identical to {@link LABKEY.Query.GetData.getRawData},
423  * except it has an additional parameter <strong><em>webPartConfig</em></strong>, which is a config object for
424  * {@link LABKEY.QueryWebPart}. Note that the Query returned from GetData is a read-only temporary query, so some
425  * features of QueryWebPart may be ignored (i.e. <em>showInsertButton</em>, <em>deleteURL</em>, etc.).
426  * @see LABKEY.QueryWebPart
427  * @see LABKEY.Query.GetData.getRawData
428  */
429 
430 
431