3

Google apps script provides a library feature, where if you include the project key, a library is added as global object. I'm looking to iterate all functions of a added library. This used to be possible in engine with a for...in loop. But I'm unable to iterate through any of the properties of to a library in engine.

The documentation says

In the V8 runtime, a project and its libraries are run in different execution contexts and hence have different globals and prototype chains.

Can anyone explain how this object is created or how to access all it's properties?

Project A:

function testLib(prop = 'main') {
  const isEnumerable = MyLibrary.propertyIsEnumerable(prop);
  const isOwnProperty = MyLibrary.hasOwnProperty(prop);
  console.info({ isEnumerable, isOwnProperty }); // { isEnumerable: false, isOwnProperty: true }
  console.info(prop in MyLibrary);//true
  for (const property in MyLibrary) {
    //loop doesn't start
    console.info(property);
  }
  console.info(Object.getOwnPropertyDescriptor(MyLibrary, prop)); //logs expected  data:
  /*{ value: [Function: main],
  writable: true,
  enumerable: false,
  configurable: true }*/
  console.log(Object.getOwnPropertyDescriptors(MyLibrary)); //actual: {} Expected: array of all functions including `main`
  MyLibrary.a = 1;
  console.log(Object.getOwnPropertyDescriptors(MyLibrary)); //actual: {a:1} Expected: array of all functions including `main`
}

function testPropDescriptors() {
  const obj = { prop1: 1, b: 2 };
  console.log(Object.getOwnPropertyDescriptors(obj)); //logs expected data
  /*{prop1: { value: 1, writable: true, enumerable: true, configurable: true },
  b: { value: 2, writable: true, enumerable: true, configurable: true } }*/
}

MyLibrary(Project B):

function main(){}
function onEdit(){}

To reproduce,

TheMaster
  • 45,448
  • 6
  • 62
  • 85
  • 2
    I suspect a `Proxy` with a broken [`ownKeys` trap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys) implementation – Bergi Nov 19 '21 at 10:10
  • @Bergi Thanks. Anyway to test your theory? Obviously, `instanceof` can't be used to test whether `MyLibrary` is a ``proxy``. Even if it is a `Proxy`, Do you see any way to iterate? – TheMaster Nov 19 '21 at 11:51
  • @TheMaster Can you get a debug connection to the script? A debugger should be able to tell whether it's a proxy (or, more likely given the environment, a native object with the same behaviour implemented in native code). – Bergi Nov 19 '21 at 14:31
  • @Bergi There is a debugger and it lists `MyLibrary` as a global object like: `MyLibrary:{}` with a "expand" object option. But the "expand" button does nothing( unlike other global objects like `Array:{}` or `Proxy:{}`, which expands to show object properties). – TheMaster Nov 19 '21 at 14:34

2 Answers2

2

Issue and workaround:

I had been looking for the methods for directly retrieving the properties and functions in the library side from the client-side under enabling V8. But unfortunately, I cannot still find it. So in my case, I use 2 workarounds.

  1. Retrieve all scripts using Apps Script API and/or Drive API.

  2. Wrapping the properties and functions in an object.

By the above workarounds, the properties and functions on the library side can be retrieved from the client-side.

Workaround 1:

In this workaround, all scripts on the library side are retrieved using Apps Script API and Drive API.

Sample script 1:

In this sample, Apps Script API is used. So, when you use this script, please link Google Cloud Platform Project to Google Apps Script Project. Ref And, please enable Apps Script API at API console.

const projectIdOflibrary = "###"; // Please set the project ID of the library.

const url = `https://script.googleapis.com/v1/projects/${projectIdOflibrary}/content`;
const res = UrlFetchApp.fetch(url, {headers: {authorization: "Bearer " + ScriptApp.getOAuthToken()}});
const obj = JSON.parse(res.getContentText())
const functions = obj.files.reduce((ar, o) => {
  if (o.name != "appsscript") ar.push(o);
  return ar;
}, []);
console.log(functions)
// console.log(functions.map(({functionSet}) => functionSet.values)) // When you want to see the function names, you can use this line.
  • When this script is used for your library script, console.log(functions.flatMap(({functionSet}) => functionSet.values)) returns [ { name: 'main' }, { name: 'onEdit' } ].

  • In this case, even when the library is the container-bound script of Google Docs, this script can work.

Sample script 2:

In this sample, Drive API is used. So, when you use this script, please enable Drive API at Advanced Google services.

const projectIdOflibrary = "###"; // Please set the project ID of the library.

const url = `https://www.googleapis.com/drive/v3/files/${projectIdOflibrary}/export?mimeType=application%2Fvnd.google-apps.script%2Bjson`;
const res = UrlFetchApp.fetch(url, {headers: {authorization: "Bearer " + ScriptApp.getOAuthToken()}});
const obj = JSON.parse(res.getContentText())
const functions = obj.files.reduce((ar, o) => {
  if (o.name != "appsscript") ar.push(o.source);
  return ar;
}, []);
console.log(functions)
  • When this script is used for your library script, console.log(functions) returns [ 'function main(){}\nfunction onEdit(){}\n' ].

  • In this case, the function names are not automatically parsed. But Google Apps Script Project is not required to be linked with Google Cloud Platform Project. But, when the library is the container-bound script of Google Docs, this script cannot be used. In this case, when the library is only the standalone type, this script can be used. Please be careful about this.

Workaround 2:

In this workaround, the properties and functions in the library side are wrapped with an object.

Sample script: Library side

var sample1 = {
  method1: function() {},
  method2: function() {}
};


var sample2 = class sample2 {
  constructor() {
    this.sample = "sample";
  }

  method1() {
    return this.sample;
  }
}

Sample script: Client side

function myFunction() {
  const res1 = MyLibrary.sample1;
  console.log(res1)

  const res2 = Object.getOwnPropertyNames(MyLibrary.sample2.prototype);
  console.log(res2)
}
  • In this case, console.log(res1) and console.log(res2) return { method1: [Function: method1], method2: [Function: method2] } and [ 'constructor', 'method1' ], respectively.

References:

Tanaike
  • 181,128
  • 11
  • 97
  • 165
  • Thanks. I had already thought about workaround#1. But I was hoping for a more direct way or some info about these objects itself or why it behaves as such. – TheMaster Nov 19 '21 at 14:31
  • @TheMaster Thank you for replying. About workaround 1, the library side is not required to be changed. When Apps Script API is used, the functions are automatically parsed. But, I think that the setting is a bit complicated. On the other hand, when Drive API is used, the setting is easier than that of Apps Script API. But the functions are not automatically parsed. So I think there are advantages and disadvantages. – Tanaike Nov 19 '21 at 23:19
  • @TheMaster So at first, I proposed to use the wrapped functions. Because I thought that in the current stage, unfortunately, it is required to modify the library side for directly achieving this question. I'm not sure that which is the better solution. I apologize for this. – Tanaike Nov 19 '21 at 23:19
  • No worries. Just wishing there was a direct way: like `Object.getOwnPropertyDescriptors()` or something like that. Appreciate your answer though – TheMaster Nov 19 '21 at 23:22
  • 1
    @TheMaster Yes. I think so. Now, I cannot find it. But when I could find it, I would like to update my answer. – Tanaike Nov 19 '21 at 23:23
2

Google Apps Script is a custom embedding of V8, so it uses the V8 C++ API to create "magic" objects. The end result is similar to a Proxy: if you know a property's name, you can retrieve it; but there's no built-in way to enumerate available functions in a library. (I have no idea why it's designed that way.)

If you control the library in question, one possible workaround is to export the list of functions from there:

// MyLibrary:
Object.defineProperty(this, "global", {value: this});
function getExports() {
  let result = [];
  let descriptors = Object.getOwnPropertyDescriptors(global);
  for (let p in descriptors) {
    if (descriptors[p].enumerable) result.push(p);
  }
  return result;
}

// main project:
console.log(MyLibrary.getExports());

(If you don't control the library, @Tanaike's answer provides some suggestions.)

jmrk
  • 34,271
  • 7
  • 59
  • 74
  • Do you have any references or sources about these "magic objects"? – TheMaster Nov 19 '21 at 23:26
  • 2
    I've looked at the source, but that source isn't public so I can't link to it. It boils down to a [NamedPropertyHandlerConfiguration](https://source.chromium.org/chromium/chromium/src/+/main:v8/include/v8-template.h;l=632;drc=59bdabfb96e4c7a0de476b24c34ff197d1fa2ce4;bpv=1;bpt=1?q=NamedPropertyHandlerConfiguration&sq=&ss=chromium%2Fchromium%2Fsrc) that has a "Getter" but no "Enumerator" callback. It _could_ be that that's just because nobody thought that might be useful; you can try filing a [feature request](https://developers.google.com/apps-script/support#missing_features). – jmrk Nov 19 '21 at 23:53
  • Is this specific for a library, because `testPropDescriptors()` logs everything correctly? – TheMaster Nov 20 '21 at 13:08
  • 2
    Yes, what I said applies to the objects that represent imported libraries. – jmrk Nov 20 '21 at 13:17