Apps Script V8: Namespaces and multiple scripts in projects

593 views
Skip to first unread message

Bruce Mcpherson

unread,
Mar 14, 2020, 11:10:14 AM3/14/20
to Google Apps Script Community
V8 brings some changes in the order in which things are done on initialization. This means that things in the global space may not behave the same as they in did in legacy apps script. 

Here's a write up of a small project that shows how namespaces avoids those kind of issues.
https://ramblings.mcpher.com/apps-script/apps-script-v8/apps-script-v8-multiple-script-files-and-namespaces/

Andrew Apell

unread,
Mar 15, 2020, 12:17:09 AM3/15/20
to Google Apps Script Community
Another topic for me to dig deeper into: Classes!
Thanks for posting this and the other insights into V8 Bruce. I'm learning something.

Jacob Jan Tuinstra

unread,
Mar 15, 2020, 11:52:35 AM3/15/20
to Google Apps Script Community
Hi Bruce,

Thanks for yet another great post ! 

My guess was that if you're willing to give up on semicolons, I might be willing to give up on function programming. In doing so, I noticed that classes in libraries aren't made available (https://stackoverflow.com/questions/60456303/can-i-use-in-google-apps-scripts-a-defined-class-in-a-library-with-es6-v8) to the user. 

Can you confirm this? And is there a work-around?

Regards from Holland,
Jacob Jan

Bruce Mcpherson

unread,
Mar 15, 2020, 1:26:24 PM3/15/20
to google-apps-sc...@googlegroups.com
Yes indeed, you are right. It seems to not only affect classes, but also arrow functions when used in libraries. The workaround is to fall back to function() declarations using var for IEF,  eg..

var Sunset = (function () {
 
  const fetcher = ({lat, lon}) => {
    const {getUrl} = Settings.apis.sunset
    const result = UrlFetchApp.fetch(getUrl(lat, lon))
    return JSON.parse(result.getContentText())
  }
  return {
    fetcher
  }
})()

and to create a small function to instantiate classes.. eg..
// create and manage an object from sheet values
class Shob {
 
  static makeValues ({data}) {
    // derive the headers from the data
    const headers = Object.keys(data.reduce((p,row)=> {
      Object.keys(row).forEach(col=>p[col]=col)
      return p
    },{}))
    // combine the headers and the values
    return [headers].concat(data.map(row=>headers.map(col=>row[col])))
  }

  static makeData ({values}) {
    const [headers,...data] = values
    return {
      data: data.map(row=>headers.reduce((p,c,i)=>{
        p[c] = row[i]
        return p
      },{})),
      headers
    }
  }

  constructor ({mySheet}) {
    this._mySheet = mySheet
    this.readValues()
  }

  // convert data to values and store
  setValues ({data}) {
    this.values =  this.constructor.makeValues({data: data || this.data})
  }
  set values (values) {
    this._values = values
  }
  get values () {
    return this._values
  }

  set headers (headers) {
    this._headers = headers
  }
  get headers () {
    return this._headers
  }

  // convert values to data and store
  setData ({values}) {
    const {headers, data} = this.constructor.makeData({values: values || this.values })
    this.headers = headers
    this.data = data
    return this.data
  }

  set data (data) {
    this._data = data
  }
  get data () {
    return this._data
  }

  get mySheet () {
    return this._mySheet
  }

  writeData (options) {
    // convert data to values and write to sheet
    const data = (options && options.data) || this.data
    this.setValues ({data})
    this.writeValues ()
  }

  writeValues (options) {
    const values = (options && options.values) || this.values
    this.mySheet.setValues({values})
  }

  readValues () {
    this.values = this.mySheet.getValues()
    this.setData ({values: this.values})
    return this.values
  }
}
function newShob (...args) {
  return new Shob(...args)
}

Here's what my updated app would when everything is in a library using these workarounds. 

const App = () => {
  // open the airports sheet


  const mySheet = v8.newMySheet(v8.Settings.sheets.airports)

  // convert sheet to object
  const shob = new v8.newShob({mySheet})

  // find all the rows with airports at altitude > 3000 metres
  const highAirports = shob.data.filter(f=>f.elevation_ft * 0.3048 > 3000)

  // get a handle to another sheet (and create it if it doesnt exist)
  const metreShob = v8.newShob({mySheetv8.newMySheet(v8.Settings.sheets.metres)})
  
  // replace current values (if any) with amended data from the other sheet
  // write out new values with metres and only airports > 2000 metres
  metreShob.writeData({datashob.data.map(row=>({
    ...row,
    elevation_metres:  row.elevation_ft * 0.3048
  })).filter(f=>f.elevation_metres>2000)})

  // lets add the sunset/sunrise for today
  metreShob.data.forEach(f=>{
    const result = v8.Sunset.fetcher({latf.latitude_deglonf.longitude_deg})
    // add some things from the weather
    f.date = new Date()
    f.sunrise = result.results.sunrise
    f.sunset = result.results.sunset
    f.dayLength = result.results.day_length

  })
  // and write it back
  metreShob.writeData()
}

Worth reporting this as an issue (check if someone else has reported it first).

I wonder if it's related to the confusion between this and globalthis I noted here

Because const doesn't work, but var does.


--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/google-apps-script-community/24d00657-9e86-4e01-92f2-582897717d4d%40googlegroups.com.

Bruce Mcpherson

unread,
Mar 16, 2020, 8:32:30 AM3/16/20
to Google Apps Script Community
Here's a simple workaround for both classes and disappearing namespaces when accessing them from a library, extracted from my updated post at 


Libraries

Note that this doesn't all work with libraries at the time of writing. If you want to expose a class from a library, you'll need to make a factory function (using the old function syntax)



class yourClass {
 ....
}
// to enable use of classes from a library
function newYourClass (...args) {
  return new yourClass (...args)
}

/// and in your main app - access it like this
const yourInstance = yourLibrary.newYourClass(arg1,arg2)


Similarily, v8 has a problem where namespaces defined with const or arrow functions are not visible in from libraries. Here's a workaround


const Settings = (() => {
  return {
   ... whatever
  }
})()
// to make this visible when a library
var LibSettings = Settings

// and in your main app - access it like this
const settings = yourLibrary.LibSettings

Jacob Jan Tuinstra

unread,
Mar 17, 2020, 12:06:03 PM3/17/20
to Google Apps Script Community
Hi Bruce,

Can I pass on an object as an argument to a class? Want to pass on files.next() to create a MyFiles class. Created a MyFolders class with succession !!
This is how I planned on doing it, but it returns undefined. 
class MyFile {  
 
static open ({file}) {
   
return file
 
}              
       
  constructor
(options) {
   
this._file = this.constructor.open(options)
 
}
               
 
get file () {
   
return this._file
 
}
Can you shed a light?

Regards Jacob Jan

On Saturday, 14 March 2020 16:10:14 UTC+1, Bruce Mcpherson wrote:

Bruce Mcpherson

unread,
Mar 17, 2020, 3:12:02 PM3/17/20
to google-apps-sc...@googlegroups.com

Can you share how you’re calling it? Options would need to have a property of .file for this to work. 

--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.

Jacob Jan Tuinstra

unread,
Mar 17, 2020, 4:13:46 PM3/17/20
to Google Apps Script Community
Hi Bruce,

This is how I've tried doing it: 
// go through the files  
 
while (files.hasNext()) {
   
   
// get google drive file
   
const file = v8.newMyFile(files.next())  

Thought of retrieving the Id first, and pass that to fetch the file through the DriveApp, but that seems odd and inefficient.

With a function I could do that, not, passing on an object?

Jacob Jan


On Tuesday, 17 March 2020 20:12:02 UTC+1, Bruce Mcpherson wrote:
Can you share how you’re calling it? Options would need to have a property of .file for this to work. 

On Tue, Mar 17, 2020 at 4:06 PM Jacob Jan Tuinstra <jaco...@familietuinstra.nl> wrote:
Hi Bruce,

Can I pass on an object as an argument to a class? Want to pass on files.next() to create a MyFiles class. Created a MyFolders class with succession !!
This is how I planned on doing it, but it returns undefined. 
class MyFile {  
 
static open ({file}) {
   
return file
 
}              
       
  constructor
(options) {
   
this._file = this.constructor.open(options)
 
}
               
 
get file () {
   
return this._file
 
}
Can you shed a light?

Regards Jacob Jan

On Saturday, 14 March 2020 16:10:14 UTC+1, Bruce Mcpherson wrote:
V8 brings some changes in the order in which things are done on initialization. This means that things in the global space may not behave the same as they in did in legacy apps script. 

Here's a write up of a small project that shows how namespaces avoids those kind of issues.
https://ramblings.mcpher.com/apps-script/apps-script-v8/apps-script-v8-multiple-script-files-and-namespaces/

--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-community+unsub...@googlegroups.com.

Bruce Mcpherson

unread,
Mar 18, 2020, 6:04:44 AM3/18/20
to Google Apps Script Community
The problem there is that your static function is expecting an object which has a property of .file (youre deconstructing the in open ({file})  whereas , files,next() returns a file object, which has no such property. In any case, there's really no need for static functions in a class unless it's going to be doing something useful that might be needed externally, but that is not related to the instance of the class itself.

So here's how to do the thing.
class MyFile {
constructor (file) {
this._file = file
}
get file () {
return this._file
}

get name () {
return this.file.getName()
}
}
const f = () => {
const files = DriveApp.getFilesByName("Untitled")
const myFiles = []
while (files.hasNext()) {
myFiles.push(new MyFile(files.next()))
}
console.log(myFiles.map(f=>f.name))
}

On Saturday, 14 March 2020 15:10:14 UTC, Bruce Mcpherson wrote:

Jacob Jan Tuinstra

unread,
Mar 18, 2020, 3:49:43 PM3/18/20
to google-apps-sc...@googlegroups.com
Hi Bruce,

Thanks for that. Didn't really pay attention to the fact that classes are new to GAS, and merely tried to copy/paste your solution. 
Will you be covering them in more detail, still? 

Regards Jacob Jan

--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/google-apps-script-community/3d63bf10-bf2b-4171-8e8b-3ef09943427e%40googlegroups.com.

Bruce Mcpherson

unread,
Mar 18, 2020, 7:38:53 PM3/18/20
to google-apps-sc...@googlegroups.com
Yes I’ll be writing up some more when I have time

Reply all
Reply to author
Forward
0 new messages