Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make the library work with the caller script PropertiesService?

Until Google extends the import/export API to container-bound Apps Script projects, I have moved most of my project to a library which can use that API, and then made Google Docs project into a shell that just calls through to the library.

My problem is having the library access the same properties (PropertiesService) as the Google Doc project. Since I have existing users of my Docs Add-on, I need to keep using these properties.

In my Google Doc project, I tried

$.PropertiesService = PropertiesService;

(where $ is my library).

It didn't work. The library kept using its own properties.

So then I tried:

function _mock(obj) {
  var ret = {};
  for(var key in obj) {
    if(typeof obj[key] == 'function') {
      ret[key] = obj[key].bind(obj);
    } else {
      ret[key] = obj[key];
    }
  }
  return ret;
}

$.PropertiesService = _mock(PropertiesService);

Still not working. Trying again:

function _mock(obj) {
  var ret = {};
  for(var key in obj) {
    if(typeof obj[key] == 'function') {
      ret[key] = (function(val) {
        return function() {
          return val.apply(obj, arguments);
        };
      })(obj[key]);
    } else {
      ret[key] = obj[key];
    }
  }
  return ret;
}

$.PropertiesService = _mock(PropertiesService);

This works.


At this point, I'm wondering:

  1. Why did the first two ways not work, but the third way did?

  2. Can I expect this to continue working?

  3. Is there a better way to have a library access the main script's properties?

Documentation is sparse. There is this, but the PropertiesService is not mentioned.

like image 429
Paul Draper Avatar asked Oct 02 '22 03:10

Paul Draper


1 Answers

Sharing of resources

As you are aware, libraries have shared and non-shared resources. PropertiesService is listed under non-shared resources, meaning that the library has its own instance of the service that is accessed when you reference it in the library code.

const getStore = () => PropertiesService.getScriptProperties();

If the function above is declared in the library, it will use the library's resource, if in the calling script - its own instance.


V8 runtime solution

V8 runtime does not create a special context for your code and gives you access to built-in services directly. Because of this when using the runtime, the service can be injected by simply defining or replacing a property on a global this:

//in the library;
var getProperty = ((ctxt) => (key) => {
    var service = ctxt.injectedService;
    var store = service.getScriptProperties();
    return store.getProperty(key);
})(this);

var setProperty = ((ctxt) => (key, val) => {
    var service = ctxt.injectedService;
    var store = service.getScriptProperties();
    return store.setProperty(key, val);
})(this);

var inject = ((ctxt) => (service) => ctxt.injectedService = service)(this);

var greet = ((ctxt) => () => {
    var store = ctxt.injectedService.getScriptProperties();
    return store.getProperty("greeting") || "Ola!";
})(this);

//in the calling script;
function testSharedResources() {
  PropertiesService.getScriptProperties().setProperty("greeting", "Hello, lib!");
  $.inject(PropertiesService);
  Logger.log($.greet()); //Hello, lib!
  $.setProperty("greeting", "Hello, world!");
  Logger.log($.greet()); //Hello, world!
}

In some contexts global this will be undefined (I encountered this when adding a library to a bound script). In this case, simply define a private global namespace (to avoid leaking to the caller script):

//in the library;
var Dependencies_ = {
    properties : PropertiesService
};

var use = (service) => {
    if ("getScriptProperties" in service) {
        Dependencies_.properties = service;
    }
};

//in the calling script;
$.use(PropertiesService);

Rhino runtime solution

Older Rhino runtime, on the other hand, creates a special implicit context. This means that you have no access to built-in services or the global this. Your only option is to bypass calling the service in the library (your approach #3 is perfect for doing so).


Questions

  1. Why did the first two ways not work, but the third way did?

All issues with your approaches boil down to:

  1. Resource sharing (libraries have their own service instances)
  2. Special implicit context (no external access to lib built-ins in Rhino)

But there is a catch: all 3 approaches do work as designed.

First, Approach one does work if you specifically reference the PropertiesService on $. This makes sense as the library is included as a namespace with members mapped to global declarations in the library. For example:

//in the caller script
PropertiesService.getScriptProperties().setProperty("test", "test");
$.PropertiesService = PropertiesService;

Logger.log( $.PropertiesService.getScriptProperties().getProperty("test") ); // "test"
Logger.log( $.getProperty("test") ); // "null"

//in the library
function getProperty(key) {
  var store = PropertiesService.getScriptProperties();
  return store.getProperty(key);
}

Approach two also works. Binding of the function in the caller script does not change the fact if called in the library it receives library context, but if you call the bound copy directly in the calling script, it works:

//in the caller script
PropertiesService.getScriptProperties().setProperty("test", "test");
var bound = $.PropertiesService.getScriptProperties.bind(PropertiesService); 

var obj = { getScriptProperties : bound };
  
$.PropertiesService = obj;

Logger.log( bound().getProperty("test") ); // "test"
Logger.log( $.getProperty("test") ); // "null"

Now, why does the third approach work out of the box? Because of the closure resulting from the wrapped function capturing the PropertiesService of the calling script and applying the getScriptProperties method. To illustrate:

//in the caller script
var appl = { 
  getScriptProperties : (function(val) { 
    return function() { 
      return val.apply(PropertiesService);
    }; 
  })(PropertiesService.getScriptProperties) 
};
  
$.PropertiesService = appl;
  
Logger.log( $.getProperty("test") ); // "test"
  1. Can I expect this to continue working?

Yes and no. Yes, because your _mock function behavior exhibits the expected behavior in all cases. No, because apply relies on the getScriptProperties not being implemented as an arrow function where this override will be ignored.

  1. Is there a better way to have library access the main script's properties?

For Rhino runtime - don't think so. For V8 - direct injection of the service will suffice.

like image 194
Oleg Valter is with Ukraine Avatar answered Oct 03 '22 21:10

Oleg Valter is with Ukraine