├── LICENSE ├── README.MD ├── create_user.gs └── script.gs /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Brecht Vermeire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Looker API for Google Sheets 2 | 3 | This Google Apps Script uses Looker API to load Looks, get data dictionaries, etc. 4 | 5 | ## Install 6 | 7 | * Save the script in the script editor *(Tools > Script Editor)* 8 | * Refresh your Sheet, you should now have a "Looker API Credentials" menu on top. 9 | * Click the menu and set your base URL, client id and client secret 10 | > **Important Note**: these are stored on the spreadsheet environment, anyone with access to the sheet will be able to use them. Switch to [userProperties](https://developers.google.com/apps-script/guides/properties) to make them user specific. This might break the formulas for users without API creds. 11 | * Go back to your spreadsheet, you can now use the custom formulas listed below 12 | 13 | 14 | ## How to use 15 | 16 | `=LOOKER_RUN_LOOK(1789, 1, 10000)` 17 | * Return the results of a saved Look in csv (`1`) or the raw SQL (`2`). The third parameter is the query limit. 18 | - Look ID (*required*) 19 | - Format (optional), `1` (results), `2` (SQL). Default value is `1` (results) 20 | - Query limit (optional), Default value is `5000`. `-1` for no limit. 21 | 22 | `=LOOKER_GET_LOOKS_BY_USER(123)` 23 | * Return all Looks from the user 24 | 25 | `=LOOKER_GET_LOOKS_BY_SPACE(456)` 26 | * Return all Looks within the given space 27 | 28 | `=LOOKER_GET_LOOKS_BY_DASHBOARD(789)` 29 | * Return all Looks on a dashboard 30 | 31 | `=LOOKER_GET_EXPLORES("model_name")` 32 | * Return all Explores within the given model 33 | 34 | `=LOOKER_GET_DATA_DICTIONARY("model_name")` 35 | * Return all dimensions and measures within all Explores within the given model 36 | 37 | `=LOOKER_GET_USERS()` 38 | * Return Looker users 39 | 40 | 41 | ## Creating new users 42 | 43 | This currently lives in a separate file. The custom function takes an email address and creates a new user, unless the user already exists. The function returns the Looker setup URL, for the user to add details and set a password. The script can be easily expanded to include e.g. a mail merge, to automatically send an email with the setup URL to the new user. 44 | 45 | `=CREATE_LOOKER_USER(email_address, roles)` 46 | * script in a separate file, creates new users with their email and (optional) assigned roles, returns setup URL for new users. For example `=CREATE_LOOKER_USER("brecht@looker.com", "1,2")`. 47 | 48 | ## Adding other formulas 49 | 50 | Looker's API is extensive and only a handful of endpoints are used here. You use other endpoints using `UrlFetchApp` ([docs](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app)). 51 | 52 | For example: 53 | 54 | ``` 55 | function LOOKER_OTHER_ENDPOINT(input) { 56 | 57 | var options = { 58 | "method": "get", 59 | "headers": { 60 | "Authorization": "token " + token 61 | } 62 | }; 63 | 64 | var response = UrlFetchApp.fetch("https://yourcompany.looker.com:19999/this/that" + input, options); 65 | 66 | return Utilities.parseCsv(response.getContentText()) 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /create_user.gs: -------------------------------------------------------------------------------- 1 | // this file is separate from the other script(s) and does not use script- 2 | // properties, just the API creds below. The script can be easily expanded 3 | // to include e.g. a mail merge to send the generated URL to the new user 4 | 5 | var BASE_URL = 'XXX'; 6 | var CLIENT_ID = 'XXX'; 7 | var CLIENT_SECRET = 'XXX'; 8 | 9 | 10 | /** 11 | * Creates a new user in Looker 12 | * 13 | * @param {string} email The user email address 14 | * @param {string} roles Comma-separated roles (e.g. "1,2,3") 15 | * @return The user setup link 16 | * @customfunction 17 | */ 18 | function CREATE_LOOKER_USER(email, roles) { 19 | if(email.match(/^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$/g) == null) { 20 | return "Not a valid email address."; 21 | } else { 22 | var existing_user = checkExistingUser(email); 23 | if (existing_user.length == 0) { 24 | Logger.log("User does not yet exist. Creating new user..."); 25 | var user_id = createNewUser(); 26 | addEmail(email, user_id); 27 | if(roles != null || roles != "") {addRoles(user_id, roles)}; 28 | var reset_token = getPasswordResetToken(user_id); 29 | var setup_url = BASE_URL.split(/(:19999)/)[0] + '/account/setup/' + reset_token; 30 | return setup_url; 31 | } 32 | else { 33 | Logger.log("No new user created, user " + email + 34 | " already exists."); 35 | return "User " + email + " already exists!"; 36 | } 37 | } 38 | } 39 | 40 | function checkExistingUser(email_address) { 41 | var options = { 42 | 'method': 'get', 43 | 'headers': { 44 | 'Authorization': 'token ' + login() 45 | } 46 | }; 47 | var existing_user = UrlFetchApp.fetch(BASE_URL + "/users/search?email=" + 48 | encodeURIComponent(email_address), options); 49 | existing_user = JSON.parse(existing_user.getContentText()); 50 | return existing_user; 51 | } 52 | 53 | function createNewUser() { 54 | var options = { 55 | 'method': 'post', 56 | 'headers': { 57 | 'Authorization': 'token ' + login() 58 | }, 59 | 'payload': JSON.stringify({}) 60 | }; 61 | var new_user = UrlFetchApp.fetch(BASE_URL + "/users", options); 62 | var user_id = parseInt(JSON.parse(new_user.getContentText()).id); 63 | return user_id; 64 | } 65 | 66 | function addEmail(email, user_id) { 67 | var options = { 68 | 'method': 'post', 69 | 'headers': { 70 | 'Authorization': 'token ' + login() 71 | }, 72 | 'payload': JSON.stringify({ 73 | 'email': email 74 | }) 75 | }; 76 | var response = UrlFetchApp.fetch(BASE_URL + "/users/" + user_id + 77 | "/credentials_email", options); 78 | } 79 | 80 | function addRoles(user_id, roles) { 81 | 82 | var roles_array = roles.toString().split(",").map(function(role) {return role.trim()}); 83 | Logger.log(roles_array); 84 | 85 | var options = { 86 | 'method': 'put', 87 | 'headers': { 88 | 'Authorization': 'token ' + login() 89 | } 90 | }; 91 | 92 | options.payload = JSON.stringify(roles_array) 93 | 94 | var response = UrlFetchApp.fetch(BASE_URL + "/users/" + user_id + 95 | "/roles", options); 96 | 97 | 98 | } 99 | 100 | function getPasswordResetToken(user_id) { 101 | var options = { 102 | 'method': 'post', 103 | 'headers': { 104 | 'Authorization': 'token ' + login() 105 | } 106 | }; 107 | var response = UrlFetchApp.fetch(BASE_URL + "/users/" + user_id + 108 | "/credentials_email/password_reset", options); 109 | var reset_url = JSON.parse(response.getContentText()).password_reset_url; 110 | var reset_token = reset_url.split('/').pop(); // get the reset token only 111 | return reset_token; 112 | } 113 | 114 | function login() { 115 | var post = { 116 | 'method': 'post' 117 | }; 118 | var response = UrlFetchApp.fetch(BASE_URL + "/login?client_id=" + 119 | CLIENT_ID + "&client_secret=" + CLIENT_SECRET, post); 120 | return JSON.parse(response.getContentText()).access_token; 121 | } 122 | -------------------------------------------------------------------------------- /script.gs: -------------------------------------------------------------------------------- 1 | // global vars that are taken from userProperties 2 | var BASE_URL; 3 | var CLIENT_ID; 4 | var CLIENT_SECRET; 5 | 6 | 7 | // add a custom menu to enter API credentials, so they don't need to be saved on the script 8 | function onOpen() { 9 | // Add API credentials menu to sheet 10 | SpreadsheetApp.getUi() 11 | .createMenu("Looker API Credentials") 12 | .addItem("Set Credentials", "setCred") 13 | .addItem("Test Credentials", "testCred") 14 | .addItem("Remove Credentials", "deleteCred") 15 | .addToUi(); 16 | } 17 | 18 | // all custom functions 19 | 20 | 21 | /** 22 | * Returns the results or the sql of a Look 23 | * 24 | * @param {number} id The unique ID of the Look 25 | * @param {number} opt_format 1 for csv, 2 for raw sql - defaults to csv (optional) 26 | * @param {number} opt_limit the query limit - defaults to 5000 if empty (optional) 27 | * @return The Look results data 28 | * @customfunction 29 | */ 30 | function LOOKER_RUN_LOOK(id, opt_format, opt_limit) { 31 | try { 32 | var options = { 33 | "method": "get", 34 | "headers": { 35 | "Authorization": "token " + login() 36 | } 37 | }; 38 | 39 | // set formatting to either csv or the raw sql query since sheets is limited 40 | var formatting; 41 | // convert param 42 | switch (opt_format) { 43 | case 1: 44 | formatting = "csv"; 45 | break; 46 | case 2: 47 | formatting = "sql"; 48 | break; 49 | default: 50 | formatting = "csv"; 51 | } 52 | 53 | // set a custom limit 54 | var limit; 55 | if(opt_limit) { 56 | limit = opt_limit; 57 | // else use the 5k default 58 | } else if (opt_limit == -1) { 59 | limit = -1; 60 | } else { 61 | limit = 5000; 62 | } 63 | 64 | // get request for the look 65 | var response = UrlFetchApp.fetch(BASE_URL + "/looks/" + id + "/run/" + formatting + "?limit=" + limit, options); 66 | 67 | // if it's csv, fill it in the cells, if it's the query, use one cell only, if not specified, throw error 68 | if (opt_format == 1) { 69 | return Utilities.parseCsv(response.getContentText()); 70 | } else if (opt_format == 2) 71 | { 72 | return response.getContentText(); 73 | } 74 | else { 75 | return Utilities.parseCsv(response.getContentText()); 76 | } 77 | } catch (err) { 78 | return "Uh oh! Something went wrong. Check your API credentials and if you're passing the correct parameters and that your Look exists!"; 79 | } 80 | } 81 | /** 82 | * Get explores for a certain model 83 | * 84 | * @param {string} id The Model Name 85 | * @return All explores in the given Model 86 | * @customfunction 87 | */ 88 | function LOOKER_GET_EXPLORES(model_name) { 89 | try { 90 | var options = { 91 | "method": "get", 92 | "headers": { 93 | "Authorization": "token " + login() 94 | } 95 | }; 96 | // just a list of explore names on the model here 97 | var response = UrlFetchApp.fetch(BASE_URL + "/lookml_models/" + model_name, options); 98 | var explores = JSON.parse(response.getContentText()).explores; 99 | 100 | // for all results we create an array and then push results to it 101 | var result = []; 102 | for (var i = 0; i < explores.length; i++) { 103 | result.push(explores[i].name); 104 | } 105 | return result 106 | } catch (err) { 107 | return "Uh oh! Something went wrong. Check your API credentials and if you're passing the correct parameters and that your model exists!" 108 | } 109 | } 110 | /** 111 | * Get all Looks by space 112 | * 113 | * @param {number} id The space ID 114 | * @return All looks in the given space 115 | * @customfunction 116 | */ 117 | function LOOKER_GET_LOOKS_BY_SPACE(space_id) { 118 | try { 119 | var options = { 120 | "method": "get", 121 | "headers": { 122 | "Authorization": "token " + login() 123 | } 124 | }; 125 | var response = UrlFetchApp.fetch(BASE_URL + "/looks/search?space_id=" + space_id.toString(), 126 | options); 127 | var looks = JSON.parse(response.getContentText()); 128 | var result = []; 129 | 130 | // push a header row first 131 | result.push(["Look ID", "Look Title", "Owner User ID", "Model Name", "Query ID"]); 132 | 133 | // loop through looks and push them to the array 134 | for (var i = 0; len = looks.length, i < len; i++) { 135 | result.push([looks[i].id, looks[i].title, looks[i].user_id, looks[i].model.id, 136 | looks[i].query_id 137 | ]); 138 | } 139 | return result 140 | } catch (err) { 141 | Logger.log(err); 142 | return "Uh oh! Something went wrong. Check your API credentials and if you're passing the correct parameters and that your space exists!" 143 | } 144 | } 145 | /** 146 | * Get all Looks by user 147 | * 148 | * @param {number} id The user ID 149 | * @return All looks created by the user 150 | * @customfunction 151 | */ 152 | function LOOKER_GET_LOOKS_BY_DASHBOARD(dashboard_id) { 153 | try { 154 | var options = { 155 | "method": "get", 156 | "headers": { 157 | "Authorization": "token " + login() 158 | } 159 | }; 160 | var response = UrlFetchApp.fetch(BASE_URL + "/dashboards/" + dashboard_id.toString(), options); 161 | var elements = JSON.parse(response.getContentText()).dashboard_elements; 162 | var result = []; 163 | 164 | // push header row first 165 | result.push(["Look ID", "Look Title"]); 166 | 167 | // loop through looks and push to array 168 | for (var i = 0; len = elements.length, i < len; i++) { 169 | if (elements[i].look_id != null) { 170 | result.push([elements[i].look_id, elements[i].title]); 171 | } 172 | } 173 | return result 174 | } catch (err) { 175 | Logger.log(err); 176 | return "Uh oh! Something went wrong. Check your API credentials and if you're passing the correct parameters and that your dashboard exists!" 177 | } 178 | } 179 | /** 180 | * Get all Looks by user 181 | * 182 | * @param {number} id The user ID 183 | * @return All looks created by the user 184 | * @customfunction 185 | */ 186 | function LOOKER_GET_LOOKS_BY_USER(user_id) { 187 | try { 188 | var options = { 189 | "method": "get", 190 | "headers": { 191 | "Authorization": "token " + login() 192 | } 193 | }; 194 | var response = UrlFetchApp.fetch(BASE_URL + "/looks/search?user_id=" + user_id.toString(), 195 | options); 196 | var looks = JSON.parse(response.getContentText()); 197 | var result = []; 198 | result.push(["Look ID", "Look Title", "Owner User ID", "Model Name", "Query ID"]); 199 | for (var i = 0; len = looks.length, i < len; i++) { 200 | result.push([looks[i].id, looks[i].title, looks[i].user_id, looks[i].model.id, 201 | looks[i].query_id 202 | ]); 203 | } 204 | return result 205 | } catch (err) { 206 | Logger.log(err); 207 | return "Uh oh! Something went wrong. Check your API credentials and if you're passing the correct parameters and that the user exists!" 208 | } 209 | } 210 | /** 211 | * Get all fields that are used within explores with the given model 212 | * 213 | * @param {string} id The model name 214 | * @return All dimensions and measures in the given model 215 | * @customfunction 216 | */ 217 | function LOOKER_GET_DATA_DICTIONARY(model_name) { 218 | try { 219 | var options = { 220 | "method": "get", 221 | "headers": { 222 | "Authorization": "token " + login() 223 | } 224 | }; 225 | var response = UrlFetchApp.fetch(BASE_URL + "/lookml_models/" + model_name, options); 226 | var explores = JSON.parse(response.getContentText()).explores; 227 | var result = []; 228 | 229 | // push header row first 230 | result.push(["Connection", "Explore Name", "View Name", "Field Type", "Name", "Label", "Type", 231 | "Description", "Hidden", "SQL", "Source" 232 | ]); 233 | 234 | // for explore in explores 235 | for (var i = 0; len = explores.length, i < len; i++) { 236 | var explore = explores[i].name; 237 | // get the explore 238 | var explore_results = UrlFetchApp.fetch(BASE_URL + "/lookml_models/" + model_name + "/explores/" + 239 | explore, options); 240 | 241 | // get connection, dimensions, measures on the explore 242 | var connection = JSON.parse(explore_results.getContentText()).connection_name; 243 | var dimensions = JSON.parse(explore_results.getContentText()).fields.dimensions; 244 | var measures = JSON.parse(explore_results.getContentText()).fields.measures; 245 | 246 | // for dimension in explore, add dimension to results 247 | for (var j = 0; j < dimensions.length; j++) { 248 | result.push([connection, explore, dimensions[j].view, "Dimension", 249 | dimensions[j].name, dimensions[j].label, dimensions[j].type, 250 | dimensions[j].description, "hidden: " + dimensions[j].hidden, (dimensions[j].sql != null ? 251 | dimensions[j].sql : ""), dimensions[j].source_file 252 | ]); 253 | } 254 | 255 | // for measure in explore, add measure to results 256 | for (var k = 0; k < measures.length; k++) { 257 | result.push([connection, explore, measures[k].view, "Measure", measures[k].name, 258 | measures[k].label, measures[k].type, measures[k].description, "hidden: " + measures[k].hidden, 259 | (measures[k].sql != null ? measures[k].sql : ""), 260 | measures[k].source_file 261 | ]); 262 | } 263 | } 264 | return result 265 | } catch (err) { 266 | return "Uh oh! Something went wrong. Check your API credentials and if you're passing the correct parameters and that your model exists!" 267 | } 268 | } 269 | 270 | /** 271 | * Get all Looker users 272 | * 273 | * @return All Looker users 274 | * @customfunction 275 | */ 276 | function LOOKER_GET_USERS() { 277 | try { 278 | var options = { 279 | "method": "get", 280 | "headers": { 281 | "Authorization": "token " + login() 282 | } 283 | }; 284 | var response = UrlFetchApp.fetch(BASE_URL + "/users", options); 285 | var users = JSON.parse(response.getContentText()); 286 | var results = []; 287 | 288 | results.push(["ID", "First name", "Last name", "Email", "Status"]); 289 | 290 | for (var i = 0; i < users.length; i++) { 291 | results.push([users[i].id, users[i].first_name, users[i].last_name, users[i].email, (users[i].is_disabled == "TRUE" ? "disabled" : "enabled")]); 292 | } 293 | return results 294 | 295 | } catch (err) { 296 | Logger.log(err); 297 | return err 298 | } 299 | } 300 | 301 | 302 | 303 | 304 | // all API credential stuff 305 | // We're using scriptProperties to store api creds script wide. 306 | // Change this to userProperties to make it userSpecific. 307 | 308 | // set credentials via prompt 309 | function setCred() { 310 | var ui = SpreadsheetApp.getUi(); 311 | var base_url_input = ui.prompt("Set your Looker API credentials", "Base URL (e.g. https://yourdomain.looker.com:19999/api/3.0):", ui.ButtonSet.OK_CANCEL); 312 | var client_id_input = ui.prompt("Set your Looker API credentials", "Client ID:", ui.ButtonSet.OK_CANCEL); 313 | var client_id_secret = ui.prompt("Set your Looker API credentials", "Client Secret:", ui.ButtonSet 314 | .OK_CANCEL); 315 | var scriptProperties = PropertiesService.getScriptProperties(); 316 | // assign them to scriptProperties so the user doesn't have to enter them over and over again 317 | scriptProperties.setProperty("BASE_URL", base_url_input.getResponseText()); 318 | scriptProperties.setProperty("CLIENT_ID", client_id_input.getResponseText()); 319 | scriptProperties.setProperty("CLIENT_SECRET", client_id_secret.getResponseText()); 320 | // test the credentials with a /user call 321 | testCred(); 322 | } 323 | 324 | // testing the existing creds 325 | function testCred() { 326 | var ui = SpreadsheetApp.getUi(); 327 | var options = { 328 | "method": "get", 329 | "headers": { 330 | "Authorization": "token " + login() 331 | } 332 | }; 333 | try { 334 | var response = UrlFetchApp.fetch(BASE_URL + "/user", options); 335 | var success_header = "Successfully set API credentials!"; 336 | var success_content = "Authenticated as " + JSON.parse(response.getContentText()).first_name + 337 | " " + JSON.parse(response.getContentText()).last_name + " (user " + JSON.parse(response.getContentText()).id +").Keep in mind that API credentials are script/spreadsheet bound. This is needed for the custom formulas to keep on working for other users. Hit 'Test' to test your credentials or 'Delete' to remove the currently set credentials."; 338 | var result = ui.alert(success_header, success_content, ui.ButtonSet.OK); 339 | } catch (err) { 340 | var result = ui.alert("Invalid credentials / Credentials not set!", 341 | "Doublecheck your base URL and your client ID & secret.", ui.ButtonSet.OK); 342 | } 343 | } 344 | 345 | // delete credentials from scriptProperties 346 | function deleteCred() { 347 | var scriptProperties = PropertiesService.getScriptProperties(); 348 | scriptProperties.deleteAllProperties(); 349 | } 350 | 351 | // login now checks for scriptProperties to ge t 352 | function login() { 353 | var scriptProperties = PropertiesService.getScriptProperties(); 354 | 355 | // load credentials from scriptProperties 356 | BASE_URL = scriptProperties.getProperty("BASE_URL"); 357 | CLIENT_ID = scriptProperties.getProperty("CLIENT_ID"); 358 | CLIENT_SECRET = scriptProperties.getProperty("CLIENT_SECRET"); 359 | 360 | try { 361 | var post = { 362 | "method": "post" 363 | }; 364 | var response = UrlFetchApp.fetch(BASE_URL + "/login?client_id=" + CLIENT_ID + "&client_secret=" + 365 | CLIENT_SECRET, post); 366 | return JSON.parse(response.getContentText()).access_token; 367 | } catch (err) { 368 | Logger.log(err); 369 | return false; 370 | } 371 | } 372 | --------------------------------------------------------------------------------