0

See the below example,

function doSomething1(){/*needs ss*/const ss = SpreadsheetApp.openById(/*SPREADSHEET_ID*/);}
function doSomething2(){/*needs ss*/const ss = SpreadsheetApp.openById(/*SPREADSHEET_ID*/);}
function doItAll(){
  doSomething1();
  doSomething2();
}

Instead of calling Spreadsheet in both functions, this could be simplified using globals as

const ss = SpreadsheetApp.openById(/*SPREADSHEET_ID*/);
function doSomething1(){/*do something with ss*/}
function doSomething2(){/*do something with ss*/}
function doItAll(){
  doSomething1();
  doSomething2();
}

The problem here can be solved without using global variables by simply passing ss variable between the functions. But This will get much more complicated with multiple functions requiring access to the ss variable. And passing ss is cumbersome. There aren't many ways to avoid a global in Apps script. Modules aren't supported. If you use a IIFE, all functions are hidden from the IDE- making a function call from IDE or anywhere else impossible. Using a global here is much more elegant. But problems arise if I have a simple trigger:

const ss = SpreadsheetApp.openById(/*SPREADSHEET_ID*/);
function doSomething1(){/*do something with ss*/}
function doSomething2(){/*do something with ss*/}
function doItAll(){
  doSomething1();
  doSomething2();
}
function onOpen(){/*Adds a menu*/}

The menu addition onOpen will fail because this line is loaded SpreadsheetApp.openById(/*SPREADSHEET_ID*/) before onOpen and that line requires permissions/authorizations while onOpen being a simple trigger doesn't run with any code requiring authorization.

How to declare globals without running into authorization errors?

TheMaster
  • 45,448
  • 6
  • 62
  • 85
  • I'm trying to understand your statements about authorization. Are you saying that since SpreadsheetApp.openById(..) requires authorization, adding an OnOpen() call means that, being global, SpreadsheetApp.openById(..) will need to run before OnOpen() and OnOpen() may be running in AuthMode.NONE? (I think your use of "this line" and "that line" is confusing me) – John Pankowicz Aug 25 '22 at 13:42
  • @JohnPankowicz Yes. – TheMaster Aug 25 '22 at 15:19
  • Is that true just for onOpen, or would it fail if the code included any trigger function? – John Pankowicz Aug 25 '22 at 18:41
  • @JohnPankowicz It will fail for any triggers that run without authorization, i.e., any simple trigger. – TheMaster Aug 25 '22 at 19:17
  • Related: https://stackoverflow.com/a/64384141 – TheMaster Aug 25 '22 at 19:20

2 Answers2

7

This issue can be solved by using a getter. A getter executes the code only when called from anywhere, thus encapsulating the execution of the code in global context. But the getter will execute on each call to the variable. If ss is called in two functions, SpreadsheetApp.openById is executed twice. We can avoid this using lazy loading technique mentioned in the MDN.

const config = {
  get ss() {
    delete this.ss;
    return (this.ss = SpreadsheetApp.openById(/*SPREADSHEET_ID*/));
  },
};
function doSomething1(){/*do something with config.ss*/}
function doSomething2(){/*do something with config.ss*/}
function doItAll(){
  doSomething1();
  doSomething2();
}
function onOpen(){/*Adds a menu*/}

Here, We use a getter inside a object instead of directly declaring the ss. Used this way, SpreadsheetApp.openById() is never called in global scope, though it is declared in global scope. It is only loaded, when doSomething1 is executed. In addition, the method is not called again when it is accessed from doSomething2, because the getter is deleted on first access and replaced with the actual value.

Although the code becomes a bit bulky, this solves many problems and is much more elegant.

Samples:

TheMaster
  • 45,448
  • 6
  • 62
  • 85
  • Hi! Got the same issue, but I'm not sure that your example applioes exactly to my issue. So, how would I go about add many more ```ss```'s? Would ```get ss1```, ```get ss2``` so on and so forth work? – onit Mar 14 '22 at 20:02
  • @santosOnit Have you read the samples in my answer? – TheMaster Mar 15 '22 at 01:38
0

I've found that you don't have to use the return here return (this.ss = SpreadsheetApp.openById(/*SPREADSHEET_ID*/)

I was messing around with this one day. I tried it this way and it makes it possible to maintain state between function calls just like real global variables without have to go through extra steps of starting up the properties service. However it does require that you have to run the readInit every time.

 let gobj={get init(){delete this.init;this.globals=getGlobals();this.init=PropertiesService.getScriptProperties().getProperties();}};
     readInit();
    
    function readInit() {
      gobj.init;
      console.log(JSON.stringify(gobj.globals));
      console.log(JSON.stringify(gobj.init))
    }

Just wondering what you think which is another way of saying what's the down side. I also added the getGlobals which is something I often access in my code. Only now I can access it with gobj.globals.key etc I haven't tried it in anything big yet so it may slow things down quite a bit.

Cooper
  • 59,616
  • 6
  • 23
  • 54
  • I think `readInit` once is enough. Infact, once you access `gobj.init`, everything should be initialized automatically. – TheMaster Feb 16 '21 at 06:49