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