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