1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2014-2017 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 LABKEY.Query = new function(impl, $) {
 20 
 21     // Insert a hidden <form> into to page, put the JSON into it, and submit it - the server's response
 22     // will make the browser pop up a dialog
 23     function submitForm(url, formData) {
 24         if (!formData['X-LABKEY-CSRF'])
 25             formData['X-LABKEY-CSRF'] = LABKEY.CSRF;
 26 
 27         var formId = LABKEY.Utils.generateUUID();
 28 
 29         var html = '<form method="POST" id="' + formId + '"action="' + url + '">';
 30         for (var name in formData)
 31         {
 32             if (!formData.hasOwnProperty(name))
 33                 continue;
 34 
 35             var value = formData[name];
 36             if (value == undefined)
 37                 continue;
 38 
 39             html += '<input type="hidden"' +
 40                     ' name="' + LABKEY.Utils.encodeHtml(name) + '"' +
 41                     ' value="' + LABKEY.Utils.encodeHtml(value) + '" />';
 42         }
 43         html += "</form>";
 44 
 45         $('body').append(html);
 46         $('form#' + formId).submit();
 47     }
 48 
 49     /**
 50      * Documentation specified in core/Query.js -- search for "@name exportSql"
 51      */
 52     impl.exportSql = function(config) {
 53 
 54         var url = LABKEY.ActionURL.buildURL("query", "exportSql", config.containerPath);
 55         var formData = {
 56             sql: config.sql,
 57             schemaName: config.schemaName,
 58             format: config.format,
 59             containerFilter: config.containerFilter
 60         };
 61 
 62         submitForm(url, formData);
 63     };
 64 
 65     /**
 66      * @private Not yet official API
 67      * Export a set of tables
 68      * @param config An object which contains the following:
 69      * @param {String} config.schemas An object with the following structure:
 70      * <pre>
 71      * {
 72      *    schemas: {
 73      *
 74      *      // export the named queries from schema "A" using the default view or the named view
 75      *      "A": [{
 76      *          queryName: "a"
 77      *          filters: [ LABKEY.Filters.create("Name", "bob", LABKEY.Filter.Types.NEQ) ],
 78      *          sort: "Name"
 79      *      },{
 80      *          queryName: "b",
 81      *          viewName: "b-view"
 82      *      }]
 83      *
 84      *    }
 85      * }
 86      * </pre>
 87      * @param {String} [config.headerType] Column header type
 88      *
 89      */
 90     impl.exportTables = function(config) {
 91 
 92         var formData = {};
 93 
 94         if (config.headerType)
 95             formData.headerType = config.headerType;
 96 
 97         // Create a copy of the schema config that we can mutate
 98         var schemas = LABKEY.Utils.merge({}, config.schemas);
 99         for (var schemaName in schemas)
100         {
101             if (!schemas.hasOwnProperty(schemaName))
102                 continue;
103 
104             var queryList = schemas[schemaName];
105             for (var i = 0; i < queryList.length; i++)
106             {
107                 var querySettings = queryList[i];
108                 var o = LABKEY.Utils.merge({}, querySettings);
109 
110                 delete o.filter;
111                 delete o.filterArray;
112                 delete o.sort;
113 
114                 // Turn the filters array into a filters map similar to LABKEY.QueryWebPart
115                 o.filters = LABKEY.Filter.appendFilterParams(null, querySettings.filters || querySettings.filterArray);
116 
117                 if (querySettings.sort)
118                     o.filters["query.sort"] = querySettings.sort;
119 
120                 queryList[i] = o;
121             }
122         }
123 
124         formData.schemas = JSON.stringify(schemas);
125 
126         var url = LABKEY.ActionURL.buildURL("query", "exportTables.view");
127         submitForm(url, formData);
128     };
129 
130     function loadingSelect(select) {
131         select.prop('disabled', true);
132         select.empty().append($('<option>', {text: 'Loading...'}));
133     }
134 
135     function populateSelect(select, options, valueProperty, textProperty, initialValue) {
136         select.empty().append($('<option>'));
137         $.each(options, function (i, option) {
138             var value = valueProperty ? option[valueProperty] : option;
139             var text = textProperty ? option[textProperty] : option;
140             var selected = initialValue && value === initialValue;
141             select.append($('<option>', { value: value,  text: text,  selected: selected}));
142         });
143 
144         select.prop('disabled', false);
145         select.on('change', function(){
146             if (initialValue != select.val())
147                 LABKEY.setDirty(true);
148         });
149     }
150 
151     function sortObjectArrayByTitle(a, b){
152         var aTitle = a.title ? a.title : a.caption;
153         var bTitle = b.title ? b.title : b.caption;
154         return aTitle.localeCompare(bTitle);
155     }
156 
157     var SCHEMA_QUERIES_CACHE = {}; // cache of queries by schema
158     function loadQueries(schemaSelect, querySelect, selectedSchema, initialValue) {
159         schemaSelect.prop('disabled', true);
160         loadingSelect(querySelect);
161 
162         if (SCHEMA_QUERIES_CACHE[selectedSchema]) {
163             populateSelect(querySelect, SCHEMA_QUERIES_CACHE[selectedSchema], 'name', 'title', initialValue);
164             schemaSelect.prop('disabled', false);
165         }
166         else {
167             LABKEY.Query.getQueries({
168                 schemaName: selectedSchema,
169                 includeColumns: false,
170                 success: function(data) {
171                     // add the sorted set of queries for this schema to the cache
172                     SCHEMA_QUERIES_CACHE[selectedSchema] = data.queries.sort(sortObjectArrayByTitle);
173 
174                     populateSelect(querySelect, SCHEMA_QUERIES_CACHE[selectedSchema], 'name', 'title', initialValue);
175                     schemaSelect.prop('disabled', false);
176 
177                     // if there is a selected query, fire the change event
178                     if (querySelect.val()) {
179                         querySelect.trigger('change');
180                     }
181                 }
182             });
183         }
184     }
185 
186     var QUERY_COLUMNS_CACHE = {}; // cache of columns by schema|query
187     function loadQueryColumns(select, schemaName, queryName, filterFn, initValue) {
188         loadingSelect(select);
189 
190         var queryKey = schemaName + '|' + queryName;
191         if (QUERY_COLUMNS_CACHE[queryKey]) {
192             populateColumnsWithFilterFn(select, QUERY_COLUMNS_CACHE[queryKey], filterFn, initValue);
193         }
194         else {
195             LABKEY.Query.getQueryDetails({
196                 schemaName: schemaName,
197                 queryName: queryName,
198                 success: function(data) {
199                     // find the default view from the views array returned
200                     // NOTE: in the future if we allow this to work for view other than the default, this logic will need to change
201                     var queryView = null;
202                     $.each(data.views, function(i, view) {
203                         if (view['default']) {
204                             queryView = view;
205                             return false;
206                         }
207                     });
208 
209                     QUERY_COLUMNS_CACHE[queryKey] = [];
210                     if (queryView) {
211                         QUERY_COLUMNS_CACHE[queryKey] = queryView.fields.sort(sortObjectArrayByTitle);
212                     }
213 
214                     populateColumnsWithFilterFn(select, QUERY_COLUMNS_CACHE[queryKey], filterFn, initValue);
215                 }
216             });
217         }
218     }
219 
220     function populateColumnsWithFilterFn(select, origFields, filterFn, initValue) {
221         var fields = [];
222         $.each(origFields, function(i, field) {
223             var includeField = true;
224 
225             // allow for a filter function to be called for each field
226             if (filterFn && LABKEY.Utils.isFunction(filterFn)) {
227                 includeField = filterFn.call(this, field);
228             }
229 
230             if (includeField) {
231                 fields.push($.extend({}, field));
232             }
233         });
234 
235         if (fields.length > 0) {
236             populateSelect(select, fields, 'name', 'caption', initValue);
237         }
238         else {
239             select.empty().append($('<option>', {text: 'No columns available'}));
240         }
241     }
242 
243     /**
244      * Documentation specified in core/Query.js -- search for "@name schemaSelectInput"
245      */
246     impl.schemaSelectInput = function(config) {
247         var SCHEMA_SELECT;
248 
249         if (!config || !config.renderTo) {
250             console.error('Invalid config object. Missing renderTo property for the <select> element.');
251             return;
252         }
253 
254         SCHEMA_SELECT = $("select[id='" + config.renderTo + "']");
255         if (SCHEMA_SELECT.length !== 1) {
256             console.error('Invalid config object. Expect to find exactly one <select> element for the renderTo provided (found: ' + SCHEMA_SELECT.length + ').');
257             return;
258         }
259 
260         loadingSelect(SCHEMA_SELECT);
261         LABKEY.Query.getSchemas({
262             includeHidden: false,
263             success: function(data) {
264                 populateSelect(SCHEMA_SELECT, data.schemas.sort(), null, null, config.initValue);
265 
266                 // if there is a selected schema, fire the change event
267                 if (SCHEMA_SELECT.val()) {
268                     SCHEMA_SELECT.trigger('change', [SCHEMA_SELECT.val()]);
269                 }
270             }
271         });
272     };
273 
274     /**
275      * Documentation specified in core/Query.js -- search for "@name querySelectInput"
276      */
277     impl.querySelectInput = function(config) {
278         var SCHEMA_SELECT, QUERY_SELECT;
279 
280         if (!config || !config.renderTo || !config.schemaInputId) {
281             var msg = 'Invalid config object. ';
282             if (!config.renderTo) {
283                 msg += 'Missing renderTo property for the <select> element. ';
284             }
285             if (!config.schemaInputId) {
286                 msg += 'Missing schemaInputId property for the parent <select> element. ';
287             }
288             console.error(msg);
289             return;
290         }
291 
292         QUERY_SELECT = $("select[id='" + config.renderTo + "']");
293         if (QUERY_SELECT.length !== 1) {
294             console.error('Invalid config object. Expect to find exactly one <select> element with the name provided (found: ' + QUERY_SELECT.length + ').');
295             return;
296         }
297 
298         SCHEMA_SELECT = $("select[id='" + config.schemaInputId + "']");
299         if (SCHEMA_SELECT.length !== 1) {
300             console.error('Invalid config object. Expect to find exactly one <select> element with the name provided (found: ' + SCHEMA_SELECT.length + ').');
301             return;
302         }
303 
304         SCHEMA_SELECT.on('change', function (event, schemaName) {
305             loadQueries(SCHEMA_SELECT, QUERY_SELECT, schemaName || event.target.value, config.initValue);
306         });
307     };
308 
309     /**
310      * Documentation specified in core/Query.js -- search for "@name columnSelectInput"
311      */
312     impl.columnSelectInput = function(config) {
313         var COLUMN_SELECT;
314 
315         if (!config || !config.renderTo || !config.schemaName || !config.queryName) {
316             var msg = 'Invalid config object. ';
317             if (!config.renderTo) {
318                 msg += 'Missing renderTo property for the <select> element. ';
319             }
320             if (!config.schemaName) {
321                 msg += 'Missing schemaName property. ';
322             }
323             if (!config.queryName) {
324                 msg += 'Missing queryName property. ';
325             }
326             console.error(msg);
327             return;
328         }
329 
330         COLUMN_SELECT = $("select[id='" + config.renderTo + "']");
331         if (COLUMN_SELECT.length !== 1) {
332             console.error('Invalid config object. Expect to find exactly one <select> element with the name provided (found: ' + COLUMN_SELECT.length + ').');
333             return;
334         }
335 
336         loadQueryColumns(COLUMN_SELECT, config.schemaName, config.queryName, config.filterFn, config.initValue);
337     };
338 
339     /**
340      * Documentation specified in core/Query.js -- search for "@name importData"
341      */
342     impl.importData = function(config) {
343         if (!window.FormData) {
344             throw new Error('modern browser required');
345         }
346 
347         var form = new FormData();
348 
349         form.append('schemaName', config.schemaName);
350         form.append('queryName', config.queryName);
351         if (config.text)
352             form.append('text', config.text);
353         if (config.path)
354             form.append('path', config.path);
355         if (config.format)
356             form.append('format', config.format);
357         if (config.module)
358             form.append('module', config.module);
359         if (config.moduleResource)
360             form.append('moduleResource', config.moduleResource);
361         if (config.importIdentity)
362             form.append('importIdentity', config.importIdentity);
363         if (config.importLookupByAlternateKey !== undefined)
364             form.append('importLookupByAlternateKey', config.importLookupByAlternateKey);
365         if (config.saveToPipeline !== undefined)
366             form.append('saveToPipeline', config.saveToPipeline);
367 
368         if (config.file) {
369             if (config.file instanceof File)
370                 form.append('file', config.file);
371             else if (config.file.tagName === 'INPUT' && config.file.files.length > 0)
372                 form.append('file', config.file.files[0]);
373         }
374 
375         return LABKEY.Ajax.request({
376             url: config.importUrl || LABKEY.ActionURL.buildURL('query', 'import.api', config.containerPath),
377             method: 'POST',
378             success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope, false),
379             failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true),
380             form: form,
381             timeout: config.timeout
382         });
383     };
384 
385     return impl;
386 
387 }(LABKEY.Query || new function() { return {}; }, jQuery);
388