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