can I have 2 functions in google.script.run? - google-apps-script

Can I have multiple apps script function in google.script.run like:
google.script.run.withSuccessHandler(function).scriptfunction1(variable).scriptfunction2(variable);

No. The .scriptfunction1(variable) function cannot return a script runner. It can only return a value, an object or array that ultimately contains primitives, or undefined.
But you can do something like this (from the documentation):
const myRunner = google.script.run.withFailureHandler(onFailure);
const myRunner1 = myRunner.withSuccessHandler(onSuccess);
const myRunner2 = myRunner.withSuccessHandler(onDifferentSuccess);
myRunner1.doSomething();
myRunner1.doSomethingElse();
myRunner2.doSomething();

Related

Finding ImportJSON Query Path in Google Sheets

Using Google Sheets with the custom IMPORTJSON function, I'm trying to parse the results of this URL so that it returns only the nodes with "highs".
Using the below formula, where cell A1 is the above URL, returns a #REF! error:
=ImportJSON(A1, "/data//highs", "noInherit, noTruncate")
I've also tried other variations of the /data//highs query (which would usually work with the IMPORTXML function) in the formula with the same result.
DESIRED OUTPUT
The query should result in displaying the records under "highs", i.e., both /data/nasdaq/highs and /data/nyse/highs.
The below image is the output for /data/nasdaq/highs. I'm looking to combine that query result with that for /data/nyse/highs in a single call.
Using ImportJSON:
One option could be to just separate both queries by a comma:
=ImportJSON(A1, "/data/nasdaq/highs,/data/nyse/highs", "noInherit, noTruncate")
This has several potential downsides, though: it won't automatically detect any additional ticket that has highs, but just nasdaq and nyse. If you wanted others, you'd have to edit your query.
Also, it will return nasdaq and nyse values in multiple columns, which I assume is not what you want.
Writing an alternative function:
Alternatively, since importJson cannot handle queries like //data/*/highs, I'd suggest writing a different custom function to handle this. To do this, select Extensions > Apps Script and copy the following function (see inline comments):
function altImportJson(url, query) {
const jsondata = UrlFetchApp.fetch(url); // Get data
let object = JSON.parse(jsondata.getContentText());
const levels = query.split("/").filter(String); // Get query levels
for (let i = 0; i < levels.length; i++) { // Iterate through query levels
const current = levels[i];
const last = levels[i-1];
const next = levels[i+1];
if (current != "*" && last != "*") object = object[current]; // Regular level, not "*"
else if (current == "*") { // Handle "*"
object = Object.values(object)
.filter(o => o[next])
.map(o => o[next])
.flat();
}
};
const headers = [...new Set(object.map(item => Object.keys(item)).flat())];
const data = object.map(item => { // Transform object to 2D array
let row = new Array(headers.length);
const entries = Object.entries(item);
entries.forEach(entry => {
const columnIndex = headers.indexOf(entry[0]);
if (columnIndex > -1) row[columnIndex] = entry[1];
});
return row;
});
data.unshift(headers);
return data;
}
After copying this function and saving your script, you can now call it:
=altImportJson(A1, "//data/*/highs")

Split column data after last delimiter into new column

I have the following data example in a google sheet:
url
https://www.testwebsite.com/compute/v1/test/images-prd-5d4d/glob/images/testimage-vsfd
https://www.testwebsite.com/compute/v1/test/images-prd-5d4d/glob/images/testimage-sdawr|
What I need is to extract the data after the substring "images/" and have something like this:
url
extract
https://www.testwebsite.com/compute/v1/test/images-prd-5d4d/glob/images/testimage-vsfd
testimage-vsfd
https://www.testwebsite.com/compute/v1/test/images-prd-5d4d/glob/images/testimage-sdawr|testimage-sdawr
I have created the following function to get this but is only extracting everything after the last "-":
function strip() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet6");
const vs = sh.getRange(2,1,sh.getLastRow() - 1).getDisplayValues().flat();
let vo = vs.map(s => [s.match(/\b[0-9A-Zaz/]+$/gi)[0]]);
sh.getRange(2,2,vo.length,1).setValues(vo);
}
What is the proper way to extract the data it's mentioned above?
You could use this on Apps Script:
function strip() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet6");
const vs = sh.getRange(2,1,sh.getLastRow() - 1).getDisplayValues().flat();
const string= "/images/";
for (i = 0; i < vs.length; i++){
//Using substrings:
const extract = vs[i].substring(vs[i].indexOf(string) + string.length);
sh.getRange(i+2,2).setValue(extract);
//Using .split():
// const extract = vs[i].split(string); //This splits the string in 2.
// sh.getRange(i+2,2).setValue(extract[1]); //Adding the second part of the array;
}
}
If you want to do it like a custom function you can try the following code:
function strip(url) {
var text = url;
var splittedValue = text.split("/images/");
return splittedValue[1];
}
It would work something like this:
Input:
Result:
The script can also be changed to get a specific range of data automatically so that every time you add a new URL you get the result in the next column automatically, but this is just for you to get the idea.
References:
Custom Functions in Google Sheets
Say your URLs are in A2:A. You can use
=arrayformula(if(isblank(A2:A),,substitute(REGEXEXTRACT(A2:A,"/images/[A-Za-z0-9-_|/\.]+"),"/images/","")))
Use native formulas where possible. That is more efficient.
If you already dealt with the issue with run time delay, and have a need to use custom function for other reasons, you can match with the "/image/" part and then remove it, or, alternatively, specifying a capturing group. Also don't forget other value characters such as _, |.

how to call a function stored in an array

I have this simple example that works
const func1 = function (i) { return (i) }
const func2 = function (i) { return (i * 2) }
const func3 = function (i) { return (i * 3) }
const func4 = function (i) { return (i * 4) }
const funcs = ['func1', 'func2', 'func3', 'func4']
function myFunction() {
let item = 3
let i = 2
let result = eval(`${funcs[item]}(${i})`)
console.log(result)
// what about call_user_func in google app script
}
Since eval() is not recommended, what will be the google app script equivalent of call_user_func (php)?
I'm unfamiliar with Google Apps Script but in standard JS you should just be able to call it like let result = funcs[item](i);
For this you also need to put your actual function variables into the array, like
const funcs = [func1, func2, func3, func4], not like const funcs = ['func1', 'func2', 'func3', 'func4']
Functions are first-class citizens in JS, which means you can pass them around and call them like any other variable
In google apps script you can also call functions by name
function executeFunctionByName(func) {
this[func]();
}

Allow script to run as admin to update spreadsheet

I have a spreadsheet that is shared with some other users. Many of the cell are range protected. However, through a menu I allow a user to run a script (which access an external library therefore invisible and not in the control of the user) that will modify some of the protected ranges. However, the script throws that there is no permission to perform this operation.
Is there any option to have this library run with library 'admin' rights so it doesn't throws due to protection?
Thx!
According to the documentation : "the library does not have its own instance of the feature/resource and instead is using the one created by the script that invoked it. "
So library is not the way to go.
You can achieve that behavior using a standalone script that would run as a service (a doGet() function in a deployed webapp) that you would deploy as "running as you" and that you would call with parameters to tell it what to do on the target spreadsheet range.
Edit : in its most basic implementation you can use a simple script like this one as a server app :
function doGet(e) {
if(e.parameter.mode==null){return ContentService.createTextOutput("error, wrong request").setMimeType(ContentService.MimeType.TEXT)};
var coord = e.parameter.coord;
var mode = e.parameter.mode;
var value = e.parameter.value;
var ss = SpreadsheetApp.openById('11myX1YX_________________FS6BesaBEnQ');
var sh = ss.getSheetByName(e.parameter.sN);
if(mode=='r'){
var sheetValue = JSON.stringify(sh.getRange(coord).getValue());
var valToReturn = ContentService.createTextOutput(sheetValue).setMimeType(ContentService.MimeType.JSON);
return valToReturn;
}
if(mode=='w'){
sh.getRange(coord).setValue(value);
return ContentService.createTextOutput(value).setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput('error').setMimeType(ContentService.MimeType.TEXT);
}
The above script should be deployed with the following parameters :
Then you can use it with a simple urlFetch like below :
var url = "https://script.google.com/macros/s/AKfycbxs9M0ib-VRmmcVJ0UUJXmHITOrWcoG8bYrK4EK7Tvl0krzsYc/exec"
function testServerLink(){
var coord = 'A3';//coordinates in A1 notation
var sheetName = 'Sheet1';
var data = 'test value';
var mode = 'w';// w for "write" and r for "read"
var write = sheetService(mode,coord,sheetName,data);
Logger.log(write);//shows the result in logger
var read = sheetService('r','A1',sheetName,data);
Logger.log(read);//shows the value that was in A1 cell
}
function sheetService(mode,coord,sheetName,data){
Logger.log(url+"?mode="+mode+"&coord="+coord+"&sN="+sheetName+"&value="+data);// shows the actual url with parameters, can be tested in a browser
var result = UrlFetchApp.fetch(url+"?mode="+mode+"&coord="+coord+"&sN="+sheetName+"&value="+data);
return result
}

How to define global variable in Google Apps Script

I see most examples from Google is they use only functions in a single giant script.
e.g. https://developers.google.com/apps-script/quickstart/macros
But in our style, we usually write all functions under a single namespace, such as
MyCompany = (MyCompany || {});
MyCompany.init = function () {
Logger.log('init');
};
function onOpen() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var menus = [{
name: "Init",
functionName: MyCompany.init
}];
spreadsheet.addMenu("Test", menus);
};
However, when I run the code above, it return
"MyCompany is not defined."
How to solve?
You might be better off using the Properties Service as you can use these as a kind of persistent global variable.
click 'file > project properties > project properties' to set a key value, or you can use
PropertiesService.getScriptProperties().setProperty('mykey', 'myvalue');
The data can be retrieved with
var myvalue = PropertiesService.getScriptProperties().getProperty('mykey');
In GAS global variables are not what they are in other languages. They are not constants nor variables available in all routines.
I thought I could use global variables for consistency amongst functions and efficiency as well. But I was wrong as pointed out by some people here at SO.
Global variable will be evaluated at each execution of a script, so not just once every time you run your application.
Global variables CAN be changed in a script (so they are not constants that cannot be changed by accident), but will be reinitialized when another script will be invoked.
There is also a speed penalty on using global variables. If within a function you use the same global variable two or more times, it will be faster to assign a local variable and use that instead.
If you want to preserve variables between all functions in your application, it might be using a cacheService will be best.
I found out that looping through all files and folders on a drive takes a LOT of time. But you can store info about files and folders within cache (or even properties) and speed up at least 100 times.
The only way I use global variables now is for some prefixes and for naming widgets.
I'm using a workaround by returning a function with an object of my global variables:
function globalVariables(){
var variables = {
sheetName: 'Sheet1',
variable1: 1,
variable2: 2
};
return variables;
}
function functionThatUsesVariable (){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(globalVariables().sheetName);
}
Global variables certainly do exist in GAS, but you must understand the client/server relationship of the environment in order to use them correctly - please see this question:
Global variables in Google Script (spreadsheet)
However this is not the problem with your code; the documentation indicates that the function to be executed by the menu must be supplied to the method as a string, right now you are supplying the output of the function:
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#addMenu%28String,Object%29
function MainMenu_Init() {
Logger.log('init');
};
function onOpen() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var menus = [{
name: "Init",
functionName: "MainMenu_Init"
}];
spreadsheet.addMenu("Test", menus);
};
I needed something similar like the question, you can store and fetch from the cache https://developers.google.com/apps-script/reference/cache/cache
Example:
// call cache service
var cache = CacheService.getScriptCache();
// get an item from the cache
var cached = cache.get("somekey");
// if exists in the cache use it
if (cached != null) {
// use it whatever you like.
}else{
// calculate/assign your data to cache
cache.put("somekey","somevalueorobject");
// you can even put cache data on TTL (time to live) in seconds.
cache.put("somekey","somevalueorobject",60);
I use this: if you declare var x = 0; before the functions declarations, the variable works for all the code files, but the variable will be declare every time that you edit a cell in the spreadsheet
For constants I am using function arrow expressions.
The footprint is similar to a variable declaration. Just add the () => when declaring, and () when calling the (function) variable.
var currentSheet = () => SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var maxAttempts = () => 10;
function myFunction(){
var sheetName = currentSheet().getName();
for (var i=0; i< maxAttempts(); i++){
trySomething(i);
}
}
var userProperties = PropertiesService.getUserProperties();
function globalSetting(){
//creating an array
userProperties.setProperty('gemployeeName',"Rajendra Barge");
userProperties.setProperty('gemployeeMobile',"9822082320");
userProperties.setProperty('gemployeeEmail'," rajbarge#hotmail.com");
userProperties.setProperty('gemployeeLastlogin',"03/10/2020");
}
var userProperties = PropertiesService.getUserProperties();
function showUserForm(){
var templete = HtmlService.createTemplateFromFile("userForm");
var html = templete.evaluate();
html.setTitle("Customer Data");
SpreadsheetApp.getUi().showSidebar(html);
}
function appendData(data){
globalSetting();
var ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data");
ws.appendRow([data.date,
data.name,
data.Kindlyattention,
data.senderName,
data.customereMail,
userProperties.getProperty('gemployeeName'),
,
,
data.paymentTerms,
,
userProperties.getProperty('gemployeeMobile'),
userProperties.getProperty('gemployeeEmail'),
Utilities.formatDate(new Date(), "GMT+05:30", "dd-MM-yyyy HH:mm:ss")
]);
}
function errorMessage(){
Browser.msgBox("! All fields are mandetory");
}